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

這篇博客仍是整理從https://github.com/LyricTian/gin-admin 這個項目中學習的golang相關知識git

做者在項目中使用了https://github.com/google/wire 作依賴注入,這個庫我以前沒有使用過,看了做者代碼中的使用,至少剛開始是看着優勢懵,不知道是作什麼,因此這篇博客主要就是整理這個包的使用github

依賴注入是什麼?

若是你搜索依賴注入,百度百科裏可能先看到的是控制反轉,下面是百度百科的解釋golang

控制反轉(Inversion of Control,縮寫爲IoC),是面向對象編程中的一種設計原則,能夠用來減低計算機代碼之間的耦合度。其中最多見的方式叫作依賴注入(Dependency Injection,簡稱DI),還有一種方式叫「依賴查找」(Dependency Lookup)。經過控制反轉,對象在被建立的時候,由一個調控系統內全部對象的外界實體將其所依賴的對象的引用傳遞給它。也能夠說,依賴被注入到對象中。shell

這樣的解釋可能仍是很差理解,因此咱們經過一個簡單的代碼來理解應該就清楚不少。編程

咱們用程序實現:小明對世界說:"hello golang"api

這裏將小明抽象爲People 說的內容抽象爲: Message 小明說 hello golang 抽象爲:Event, 代碼以下:bash

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/wireapp

上面的代碼,咱們用wire的方式實現,代碼以下:ide

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
}

延伸閱讀

相關文章
相關標籤/搜索