從別人的程式碼中學習golang系列--02

syncd發表於2020-07-03

這篇部落格還是整理從https://github.com/LyricTian/gin-admin 這個專案中學習的golang相關知識

作者在專案中使用了https://github.com/google/wire 做依賴注入,這個庫我之前沒有使用過,看了作者程式碼中的使用,至少剛開始是看著優點懵,不知道是做什麼,所以這篇部落格主要就是整理這個包的使用

依賴注入是什麼?

如果你搜尋依賴注入,百度百科裡可能先看到的是控制反轉,下面是百度百科的解釋

控制反轉(Inversion of Control,縮寫為IoC),是物件導向程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還有一種方式叫“依賴查詢”(Dependency Lookup)。通過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體將其所依賴的物件的引用傳遞給它。也可以說,依賴被注入到物件中。

這樣的解釋可能還是不好理解,所以我們通過一個簡單的程式碼來理解應該就清楚很多。

我們用程式實現:小明對世界說:"hello golang"

這裡將小明抽象為People 說的內容抽象為: Message 小明說 hello golang 抽象為:Event, 程式碼如下:

package main

import "fmt"

var msg = "Hello World!"

func NewMessage() Message {
   return Message(msg)
}

// 要說的內容的抽象
type Message string

func NewPeople(m Message) People {
   return People{name: "小明", message: m}
}

// 小明這個人的抽象
type People struct {
   name    string
   message Message
}

// 小明這個人會說話
func (p People) SayHello() string {
   msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message)
   return msg
}

func NewEvent(p People) Event {
   return Event{people: p}
}

// 小明去說話這個行為抽象為一個事件
type Event struct {
   people People
}

func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}

func main() {
   message := NewMessage()
   people := NewPeople(message)
   event := NewEvent(people)
   event.start()
}

從上面這個程式碼我們可以看出,我們必須先初始化一個NewMessage, 因為NewPeople 依賴它,NewEvent 依賴NewPeople. 這還是一種比較簡單的依賴關係,實際生產的依賴關係可能會更復雜,那麼什麼好的辦法來處理這種依賴,https://github.com/google/wire 就是來幹這件事情的。

wire依賴注入例子

栗子1

安裝: go get github.com/google/wire/cmd/wire

上面的程式碼,我們用wire的方式實現,程式碼如下:

package main

import (
   "fmt"

   "github.com/google/wire"
)

var msg = "Hello World!"

func NewMessage() Message {
   return Message(msg)
}

// 要說的內容的抽象
type Message string

func NewPeople(m Message) People {
   return People{name: "小明", message: m}
}

// 小明這個人的抽象
type People struct {
   name    string
   message Message
}

// 小明這個人會說話
func (p People) SayHello() string {
   msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message)
   return msg
}

func NewEvent(p People) Event {
   return Event{people: p}
}

// 小明去說話這個行為抽象為一個事件
type Event struct {
   people People
}

func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}

func InitializeEvent() Event {
   wire.Build(NewEvent, NewPeople, NewMessage)
   return Event{}
}

func main() {
   e := InitializeEvent()
   e.start()
}

這裡我們不用再手動初始化NewEvent, NewPeople, NewMessage,而是通過需要初始化的函式傳遞給wire.Build , 這三者的依賴關係,wire 會幫我們處理,我們通過wire . 的方式生成程式碼:

➜  useWireBaseExample2 wire .
wire: awesomeProject/202006/useWireBaseExample2: wrote /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample2/wire_gen.go
➜  useWireBaseExample2 

會在當前目錄下生成wire_gen.go的程式碼,內容如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

import (
   "fmt"
)

// Injectors from main.go:

func InitializeEvent() Event {
   message := NewMessage()
   people := NewPeople(message)
   event := NewEvent(people)
   return event
}

// main.go:

var msg = "Hello World!"

func NewMessage() Message {
   return Message(msg)
}

// 要說的內容的抽象
type Message string

func NewPeople(m Message) People {
   return People{name: "小明", message: m}
}

// 小明這個人的抽象
type People struct {
   name    string
   message Message
}

// 小明這個人會說話
func (p People) SayHello() string {
   msg2 := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message)
   return msg2
}

func NewEvent(p People) Event {
   return Event{people: p}
}

// 小明去說話這個行為抽象為一個事件
type Event struct {
   people People
}

func (e Event) start() {
   msg2 := e.people.SayHello()
   fmt.Println(msg2)
}

func main() {
   e := InitializeEvent()
   e.start()
}

程式碼中wire為我們生成了如下程式碼:

// Injectors from main.go:

func InitializeEvent() Event {
   message := NewMessage()
   people := NewPeople(message)
   event := NewEvent(people)
   return event
}

在看看我們剛開始寫的程式碼,發現其實是一樣的,是不是感覺方便了很多。

注意:當使用 Wire 時,我們將同時提交 Wire.go 和 Wire _ gen 到程式碼倉庫

wire 能做的事情很多,如果我們相互依賴的初始化其中有初始化失敗的,wire也能幫我們很好的處理。

栗子2

package main

import (
   "errors"
   "fmt"
   "os"
   "time"

   "github.com/google/wire"
)

var msg = "Hello World!"

func NewMessage() Message {
   return Message(msg)
}

// 要說的內容的抽象
type Message string

func NewPeople(m Message) People {
   var grumpy bool
   if time.Now().Unix()%2 == 0 {
      grumpy = true
   }
   return People{name: "小明", message: m, grumpy: grumpy}
}

// 小明這個人的抽象
type People struct {
   name    string
   message Message
   grumpy  bool // 脾氣是否暴躁
}

// 小明這個人會說話
func (p People) SayHello() string {
   if p.grumpy {
      // 脾氣暴躁,心情不好
      msg := "Go away !"
      return msg
   }
   msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message)
   return msg

}

func NewEvent(p People) (Event, error) {
   if p.grumpy {
      return Event{}, errors.New("could not create event: event greeter is grumpy")
   }
   return Event{people: p}, nil
}
https://github.com/LyricTian/gin-admin
// 小明去說話這個行為抽象為一個事件
type Event struct {
   people People
}

func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}

func InitializeEvent() (Event, error) {
   wire.Build(NewEvent, NewPeople, NewMessage)
   return Event{}, nil
}

func main() {
   e, err := InitializeEvent()
   if err != nil {
      fmt.Printf("failed to create event: %s\n", err)
      os.Exit(2)
   }
   e.start()
}

更改之後的程式碼初始化NewEvent 可能就會因為People.grumpy 的值而失敗,通過wire生成之後的程式碼

// Injectors from main.go:

func InitializeEvent() (Event, error) {
   message := NewMessage()
   people := NewPeople(message)
   event, err := NewEvent(people)
   if err != nil {
      return Event{}, err
   }
   return event, nil
}

栗子3

我們再將上面的程式碼進行更改:

package main

import (
   "errors"
   "fmt"
   "os"
   "time"

   "github.com/google/wire"
)

func NewMessage(msg string) Message {
   return Message(msg)
}

// 要說的內容的抽象
type Message string

func NewPeople(m Message) People {
   var grumpy bool
   if time.Now().Unix()%2 == 0 {
      grumpy = true
   }
   return People{name: "小明", message: m, grumpy: grumpy}
}

// 小明這個人的抽象
type People struct {
   name    string
   message Message
   grumpy  bool // 脾氣是否暴躁
}

// 小明這個人會說話
func (p People) SayHello() string {
   if p.grumpy {
      // 脾氣暴躁,心情不好
      msg := "Go away !"
      return msg
   }
   msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message)
   return msg

}

func NewEvent(p People) (Event, error) {
   if p.grumpy {
      return Event{}, errors.New("could not create event: event greeter is grumpy")
   }
   return Event{people: p}, nil
}

// 小明去說話這個行為抽象為一個事件
type Event struct {
   people People
}

func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}

func InitializeEvent(msg string) (Event, error) {
   wire.Build(NewEvent, NewPeople, NewMessage)
   return Event{}, nil
}

func main() {
   msg := "Hello Golang"https://github.com/LyricTian/gin-admin
   e, err := InitializeEvent(msg)
   if err != nil {
      fmt.Printf("failed to create event: %s\n", err)
      os.Exit(2)
   }
   e.start()
}

上面的更改主要是NewPeople 函式增加了msg引數,同時InitializeEvent增加了msg引數,這個時候我們通過wire生成程式碼則可以看到如下:

// Injectors from main.go:

func InitializeEvent(msg string) (Event, error) {
	message := NewMessage(msg)
	people := NewPeople(message)
	event, err := NewEvent(people)
	if err != nil {
		return Event{}, err
	}
	return event, nil
}

wire 會檢查注入器的引數,並檢查到NewMessage 需要msg的引數,所以它將msg傳遞給了NewMessage

栗子4

如果我們傳給wire.Build 的依賴關係存在問題,wire會怎麼處理呢? 我們調整InitializeEvent 的程式碼:

func InitializeEvent(msg string) (Event, error) {
   wire.Build(NewEvent, NewMessage)
   return Event{}, nil
}

然後執行wire 進行程式碼的生成:

➜  useWireBaseExample4 wire .
wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:63:1: inject InitializeEvent: no provider found for awesomeProject/202006/useWireBaseExample4.People
        needed by awesomeProject/202006/useWireBaseExample4.Event in provider "NewEvent" (/home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:46:6)
wire: awesomeProject/202006/useWireBaseExample4: generate failed
wire: at least one generate failure
➜  useWireBaseExample4 

錯誤提示中非常清楚的告訴我它找不到no provider found ,如果我們傳給wire.Build 沒有用的依賴,它依然會給我們提示告訴我們 unused provider "main.NewEventNumber"

➜  useWireBaseExample4 wire .
wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:67:1: inject InitializeEvent: unused provider "main.NewEventNumber"
wire: awesomeProject/202006/useWireBaseExample4: generate failed
wire: at least one generate failure

wire的高階用法

Binding Interfaces

依賴注入通常用於繫結介面的具體實現。通過下面的例子理解:

// Run 執行服務
func Run(ctx context.Context, opts ...Option) error {
	var state int32 = 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, opts...)
	if err != nil {
		return err
	}

EXIT:
	for {
		sig := <-sc
		logger.Printf(ctx, "接收到訊號[%s]", sig.String())
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			atomic.CompareAndSwapInt32(&state, 1, 0)
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}

	cleanFunc()
	logger.Printf(ctx, "服務退出")
	time.Sleep(time.Second)
	os.Exit(int(atomic.LoadInt32(&state)))
	return nil
}package main

import (
	"fmt"

	"github.com/google/wire"
)

type Fooer interface {
	Foo() string
}

type MyFooer string

func (b *MyFooer) Foo() string {
	return string(*b)
}

func provideMyFooer() *MyFooer {
	b := new(MyFooer)
	*b = "Hello, World!"
	return b
}

type Bar string

func provideBar(f Fooer) string {
	// f will be a *MyFooer.
	return f.Foo()
}


func InitializeEvent() string {
	wire.Build(provideMyFooer, provideBar, wire.Bind(new(Fooer), new(*MyFooer)))
	return ""
}
func main() {
	ret := InitializeEvent()
	fmt.Println(ret)
}

我們可以看到Fooer 是一個interface, MyFooer 實現了Fooer 這個介面,同時provideBar 的引數是Fooer 介面型別。可以看到// Run 執行服務

func Run(ctx context.Context, opts ...Option) error {
	var state int32 = 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, opts...)
	if err != nil {
		return err
	}

EXIT:
	for {
		sig := <-sc
		logger.Printf(ctx, "接收到訊號[%s]", sig.String())
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			atomic.CompareAndSwapInt32(&state, 1, 0)
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}
	logger.Printf(ctx, "服務退出")
	time.Sleep(time.Second)
	os.Exit(int(atomic.LoadInt32(&state)))
	return nil
}

程式碼中我們用了wire.Bind方法,為什麼這麼用呢?如果我們wire.Build的那段程式碼寫成如下:

wire.Build(provideMyFooer, provideBar),再次用wire生成程式碼則會提示如下錯誤:https://github.com/LyricTian/gin-admin

➜  useWireBaseExample5 wire .
wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample5/main.go:36:1: inject InitializeEvent: no provider found for awesomeProject/202006/useWireBaseExample5.Fooer
        needed by string in provider "provideBar" (/home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample5/main.go:27:6)
wire: awesomeProject/202006/useWireBaseExample5: generate failed
wire: at least one generate failure

這是因為我們傳遞給provideBar 需要的是 Fooer 介面型別,我們傳給wire.Build 的是provideMyFooer, provideBar 這個時候預設從依賴關係裡,provideBar 沒有找能夠提供Fooer的provider, 雖然我們我們都知道MyFooer 實現了Fooer 這個介面。所以我們需要在wire.Build 裡告訴它,我們傳遞provideMyFooer 就是provideBar的provider。wire.Bind 就是來做這件事情的。

wire.Bind 的第一個引數是介面型別的值的指標,第二個引數是實現第一個引數介面的型別的值的指標。

這樣當我們在用wire生成程式碼的時候就正常了。

Struct Providers

wire還可以用於結構體的構造。先直接看使用的例子:

package main

import (
   "fmt"

   "github.com/google/wire"
)

type Foo int
type Bar int

func ProvideFoo() Foo {
   return Foo(1)
}

func ProvideBar() Bar {
   return Bar(2)// Run 執行服務
func Run(ctx context.Context, opts ...Option) error {
	var state int32 = 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, opts...)
	if err != nil {
		return err
	}

EXIT:
	for {
		sig := <-sc
		logger.Printf(ctx, "接收到訊號[%s]", sig.String())
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			atomic.CompareAndSwapInt32(&state, 1, 0)
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}

	cleanFunc()
	logger.Printf(ctx, "服務退出")
	time.Sleep(time.Second)
	os.Exit(int(atomic.LoadInt32(&state)))
	return nil
}
}

type FooBar struct {
   MyFoo Foo
   MyBar Bar
}

var Set = wire.NewSet(
   ProvideFoo,
   ProvideBar,
   wire.Struct(new(FooBar), "MyFoo", "MyBar"),
)

func injectFooBar() FooBar {
   wire.Build(Set)
   return FooBar{}
}

func main() {
   fooBar := injectFooBar()
   fmt.Println(fooBar)
}

上面的例子其實很簡單,我們構造FooBar 結構題我們需要MyFooMyBar ,而ProvideFooProvideBar 就是用於生成MyFooMyBarwire.Struct 也可以幫我們做這件事情。我們通過wire生成的程式碼如下:

// Injectors from main.go:

func injectFooBar() FooBar {
   foo := ProvideFoo()
   bar := ProvideBar()
   fooBar := FooBar{
      MyFoo: foo,
      MyBar: bar,
   }
   return fooBar
}

wire.Struct 的第一個引數是所需結構型別的指標,後面的引數是要注入的欄位的名稱。可以使用一個特殊的字串“ * ”作為告訴注入器注入所有欄位的快捷方式。 所以我們上面的程式碼也可以寫成:wire.Struct(new(FooBar), "×") ,而當我們使用* 這種方式的時候可能會把一些不需要注入的欄位注入了,如鎖,那麼類似這種情況,如果我們注入,卡一通過wire:"-" 的方式告訴wire 該欄位不進行注入。

type Foo struct {
    mu sync.Mutex `wire:"-"`
    Bar Bar
}

Binding Values

這個功能主要就是給資料型別繫結一個預設值,程式碼例子如下:

https://github.com/LyricTian/gin-adminpackage main

import (
   "fmt"

   "github.com/google/wire"
)

type Foo struct {
   X int
}

func injectFoo() Foo {
   wire.Build(wire.Value(Foo{X: 11}))
   return Foo{}
}

func main() {
   foo := injectFoo()
   fmt.Println(foo)
}

我通過wire生成的程式碼如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

import (
   "fmt"
)

// Injectors from main.go:

func injectFoo() Foo {
   foo := _wireFooValue
   return foo
}

var (
   _wireFooValue = Foo{X: 11}
)

// main.go:

type Foo struct {
   X int
}

func main() {
   foo := injectFoo()
   fmt.Println(foo)
}

Use Fields of a Struct as Providers

有時,我們需要獲取結構體的某些欄位,按照我們已經使用的wire的用法,你可能會這樣寫程式碼:

package main

import (
   "fmt"

   "github.com/google/wire"
)

type Foo struct {
   S string
   N int
   F float64
}

func getS(foo Foo) string {
   return foo.S
}

func provideFoo() Foo {
   return Foo{S: "Hello, World!", N: 1, F: 3.14}
}

func injectedMessage() string {
   wire.Build(
      provideFoo,
      getS,
   )
   return ""
}

func main() {
   ret := injectedMessage()
   fmt.Println(ret)
}

這種用法當然也可以實現,但是wire其實提供了更好的辦法來實現wire.FieldsOf, 我們將上面的程式碼進行更改如下,通過wire生成的程式碼其實和上面的是一樣的:

package main

import (
   "fmt"

   "github.com/google/wire"
)

type Foo struct {
   S string
   N int
   F float64
}

func provideFoo() Foo {
   return Foo{S: "Hello, World!", N: 1, F: 3.14}
}

func injectedMessage() string {
   wire.Build(
      provideFoo,
      wire.FieldsOf(new(Foo), "S"),
   )
   return ""
}

func main() {
   ret := injectedMessage()
   fmt.Println(ret)
}

Cleanup functions

如果我們的Provider建立了一個需要做clean 的值,例如關閉檔案,關閉資料連線..., 這裡也是可以返回一個閉包來清理資源,注入器將使用它向呼叫者返回一個聚合的清理函式,或者如果稍後在注入器實現中呼叫的提供程式返回一個錯誤,則使用它來清理資源。

關於這個功能的使用,通過https://github.com/LyricTian/gin-admin 的程式碼中的使用,可以更加清楚。

作者在gin-admin/internal/app/app.go 中進行了初始化依賴注入器

// 初始化依賴注入器
injector, injectorCleanFunc, err := injector.BuildInjector()
if err != nil {
   return nil, err
}

我們在看看下wire生成的wire_gen.go程式碼:

// Injectors from wire.go:

func BuildInjector() (*Injector, func(), error) {
   auther, cleanup, err := InitAuth()
   if err != nil {
      return nil, nil, err
   }
   db, cleanup2, err := InitGormDB()
   if err != nil {
      cleanup()
      return nil, nil, err
   }
   role := &model.Role{
      DB: db,
   }
   roleMenu := &model.RoleMenu{
      DB: db,
   }
   menuActionResource := &model.MenuActionResource{
      DB: db,
   }
   user := &model.User{
      DB: db,
   }
   userRole := &model.UserRole{
      DB: db,
   }
   casbinAdapter := &adapter.CasbinAdapter{
      RoleModel:         role,
      RoleMenuModel:     roleMenu,
      MenuResourceModel: menuActionResource,
      UserModel:         user,
      UserRoleModel:     userRole,
   }
   syncedEnforcer, cleanup3, err := InitCasbin(casbinAdapter)
   if err != nil {
      cleanup2()
      cleanup()
      return nil, nil, err
   }
   demo := &model.Demo{
      DB: db,
   }
   bllDemo := &bll.Demo{
      DemoModel: demo,
   }
   apiDemo := &api.Demo{
      DemoBll: bllDemo,
   }
   menu := &model.Menu{
      DB: db,
   }
   menuAction := &model.MenuAction{
      DB: db,
   }
   login := &bll.Login{
      Auth:            auther,
      UserModel:       user,
      UserRoleModel:   userRole,
      RoleModel:       role,
      RoleMenuModel:   roleMenu,
      MenuModel:       menu,
      MenuActionModel: menuAction,
   }
   apiLogin := &api.Login{
      LoginBll: login,
   }
   trans := &model.Trans{
      DB: db,
   }
   bllMenu := &bll.Menu{
      TransModel:              trans,
      MenuModel:               menu,
      MenuActionModel:         menuAction,
      MenuActionResourceModel: menuActionResource,
   }
   apiMenu := &api.Menu{
      MenuBll: bllMenu,
   }
   bllRole := &bll.Role{
      Enforcer:      syncedEnforcer,
      TransModel:    trans,
      RoleModel:     role,
      RoleMenuModel: roleMenu,
      UserModel:     user,
   }
   apiRole := &api.Role{
      RoleBll: bllRole,
   }
   bllUser := &bll.User{
      Enforcer:      syncedEnforcer,
      TransModel:    trans,
      UserModel:     user,
      UserRoleModel: userRole,
      RoleModel:     role,
   }
   apiUser := &api.User{
      UserBll: bllUser,
   }
   routerRouter := &router.Router{
      Auth:           auther,
      CasbinEnforcer: syncedEnforcer,
      DemoAPI:        apiDemo,
      LoginAPI:       apiLogin,
      MenuAPI:        apiMenu,
      RoleAPI:        apiRole,
      UserAPI:        apiUser,
   }
   engine := InitGinEngine(routerRouter)
   injector := &Injector{
      Engine:         engine,
      Auth:           auther,
      CasbinEnforcer: syncedEnforcer,
      MenuBll:        bllMenu,
   }
   return injector, func() {
      cleanup3()
      cleanup2()
      cleanup()
   }, nil
}

而當程式退出的時候這上面程式碼返回的那些清理操作都會被執行:

// Run 執行服務
func Run(ctx context.Context, opts ...Option) error {
   var state int32 = 1
   sc := make(chan os.Signal, 1)
   signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
   cleanFunc, err := Init(ctx, opts...)
   if err != nil {
      return err
   }

EXIT:
   for {
      sig := <-sc
      logger.Printf(ctx, "接收到訊號[%s]", sig.String())
      switch sig {
      case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
         atomic.CompareAndSwapInt32(&state, 1, 0)
         break EXIT
      case syscall.SIGHUP:
      default:
         break EXIT
      }
   }
   // 在這裡執行了清理工作
   cleanFunc()
   logger.Printf(ctx, "服務退出")
   time.Sleep(time.Second)
   os.Exit(int(atomic.LoadInt32(&state)))
   return nil
}

延伸閱讀

相關文章