Skip to content

Wire

wire เป็นเครื่องมือ dependency injection ที่เปิดแหล่งโดย Google แนวคิด dependency injection นี้ได้รับความนิยมในเฟรมเวิร์ก Spring ของ Java ใน Go ก็มีไลบรารี dependency injection เช่นกัน เช่น dig ที่เปิดแหล่งโดย Uber อย่างไรก็ตาม แนวคิด dependency injection ของ wire ไม่ได้ใช้กลไก reflection ของภาษา อย่างเข้มงวดแล้ว wire เป็นตัวสร้างโค้ด แนวคิด dependency injection แสดงเฉพาะในการใช้งาน หากมีปัญหาจะพบได้ในช่วงการสร้างโค้ด

ที่อยู่ repository: google/wire: Compile-time Dependency Injection for Go (github.com)

ที่อยู่เอกสาร: wire/docs/guide.md at main · google/wire (github.com)

การติดตั้ง

ติดตั้งเครื่องมือสร้างโค้ด

go
go install github.com/google/wire/cmd/wire@latest

ติดตั้ง dependency ซอร์สโค้ด

go get github.com/google/wire

การเริ่มต้น

ใน wire การ inject dependency ขึ้นอยู่กับสององค์ประกอบ provider และ injector

provider สามารถเป็น constructor ที่ผู้พัฒนาให้ไว้ ดังนี้ Provider ต้องเปิดเผยภายนอก

go
package foobarbaz

type Foo struct {
    X int
}

// สร้าง Foo
func ProvideFoo() Foo {
    return Foo{X: 42}
}

มีพารามิเตอร์

go
package foobarbaz

// ...

type Bar struct {
    X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

หรือมีพารามิเตอร์และค่าส่งกลับ

go
package foobarbaz

import (
    "context"
    "errors"
)

type Baz struct {
    X int
}

// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("cannot provide baz when bar is zero")
    }
    return Baz{X: bar.X}, nil
}

ยังสามารถรวม proiver เข้าด้วยกัน

go
package foobarbaz

import (
    // ...
    "github.com/google/wire"
)

// ...

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

TIP

wire มีข้อกำหนดสำหรับค่าส่งกลับของ provider ดังนี้

  • ค่าส่งกลับแรกคือค่าที่ provider ให้
  • ค่าส่งกลับที่สองต้องเป็น func() | error
  • ค่าส่งกลับที่สาม หากค่าส่งกลับที่สองเป็น func แล้ว ค่าส่งกลับที่สามต้องเป็น error

injector เป็นฟังก์ชันที่ wire สร้างขึ้น รับผิดชอบเรียก provider ตามลำดับที่กำหนด ลายเซ็นของ injector กำหนดโดยผู้พัฒนา wire สร้างเนื้อหาฟังก์ชันเฉพาะ ผ่านการเรียก wire.Build เพื่อประกาศ คำประกาศนี้ไม่ควรถูกเรียก ไม่ควรถูกคอมไพล์

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import (
    "context"

    "github.com/google/wire"
    "example.com/foobarbaz"
)

// injector ที่กำหนด
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

จากนั้นดำเนินการ

wire

จะสร้าง wire_gen.go เนื้อหาดังนี้

go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//+build !wireinject

package main

import (
    "example.com/foobarbaz"
)

// injector ที่สร้างจริง
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    foo := foobarbaz.ProvideFoo()
    bar := foobarbaz.ProvideBar(foo)
    baz, err := foobarbaz.ProvideBaz(ctx, bar)
    if err != nil {
        return foobarbaz.Baz{}, err
    }
    return baz, nil
}

โค้ดที่สร้างแทบไม่มี dependency กับ wire สามารถทำงานได้ปกติโดยไม่ต้องใช้ wire และในภายหลังดำเนินการ go generate ก็สามารถสร้างอีกครั้ง หลังจากนั้น ผู้พัฒนาเรียก injector ที่สร้างจริงและส่งพารามิเตอร์ที่เกี่ยวข้องเพื่อทำ dependency injection กระบวนการทั้งหมดของโค้ดง่ายมาก รู้สึกเหมือนเพียงให้ constructor สองสามตัว แล้วสร้างฟังก์ชันที่เรียก constructor สุดท้ายเรียกฟังก์ชันนี้ส่งพารามิเตอร์ ดูเหมือนไม่ได้ทำอะไรซับซ้อนมาก เขียนด้วยมือก็ได้ ใช่แล้ว wire ทำเรื่องง่ายแบบนี้ เพียงแต่เปลี่ยนจากการเขียนด้วยมือเป็นการสร้างอัตโนมัติ ตามแนวคิดของ wire dependency injection ควรเป็นเรื่องง่ายแบบนี้ ไม่ควรทำให้ซับซ้อน

ตัวอย่าง

ด้านล่างนี้ผ่านกรณีศึกษาเพื่อเพิ่มความเข้าใจ นี่คือตัวอย่างการเริ่มต้น app

HttpServer provider รับพารามิเตอร์ net.Addr ส่งกลับพอยน์เตอร์และ error

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

func NewHttpserver(addr net.Addr) (*HttpServer, error) {
  return &HttpServer{addr}, nil
}

MysqlClient และ System provider ด้านล่างนี้ก็เช่นกัน

go
var DataBaseProviderSet = wire.NewSet(NewMysqlClient)

type MysqlClient struct {
}

var SystemSet = wire.NewSet(NewApp)

type System struct {
  server *HttpServer
  data   *MysqlClient
}

func (s *System) Run() {
  log.Printf("app run on %s", s.server.String())
}

func NewApp(server *HttpServer, data *MysqlClient) (System, error) {
  return System{server: server, data: data}, nil
}

หลังจากกำหนด provider แล้ว ต้องกำหนด injector ควรสร้างไฟล์ wire.go ใหม่เพื่อกำหนด

go
//go:build wireinject
// +build wireinject

package main

import (
  "github.com/google/wire"
  "net"
)

// กำหนด injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  // เรียก provider ตามลำดับ
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject เพื่อละเว้น injector นี้ขณะคอมไพล์ จากนั้นดำเนินการคำสั่งต่อไปนี้ หากมีผลลัพธ์ดังนี้แสดงว่าสร้างสำเร็จ

sh
$ wire
$ wire: golearn: wrote /golearn/wire_gen.go

โค้ดหลังสร้างดังนี้

go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
  "net"
)

// Injectors from wire.go:

// กำหนด injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  httpServer, err := NewHttpserver(serverAddr)
  if err != nil {
    return System{}, err
  }
  mysqlClient, err := NewMysqlClient(dataAddr)
  if err != nil {
    return System{}, err
  }
  system, err := NewApp(httpServer, mysqlClient)
  if err != nil {
    return System{}, err
  }
  return system, nil
}

จะเห็นว่าตรรกะชัดเจน ลำดับการเรียกก็ถูกต้อง สุดท้ายผ่าน injector ที่สร้างเพื่อเริ่มต้น app

go
package main

import (
  "github.com/google/wire"
  "log"
  "net"
  "net/netip"
)

func main() {
  server, err := initSystemServer(
    net.TCPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:8080")),
    "mysql:localhost:3306/test")
  if err != nil {
    panic(err)
  }
  server.Run()
}

สุดท้ายผลลัพธ์ดังนี้

2023/08/01 19:20:48 app run on 127.0.0.1:8080

นี่เป็นกรณีการใช้งานที่ง่ายมาก

การใช้งานขั้นสูง

การผูกอินเทอร์เฟซ

บางครั้งในการ inject dependency จะ inject การใช้งานเฉพาะลงในอินเทอร์เฟซ wire ในการ inject dependency จับคู่ตามประเภท

go
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()
}

พารามิเตอร์ของ provider provideBar เป็นประเภทอินเทอร์เฟซ ที่จริงแล้วคือ *MyFooer เพื่อให้ขณะสร้างโค้ด provider สามารถจับคู่ได้อย่างถูกต้อง เราสามารถผูกสองประเภทเข้าด้วยกัน ดังนี้

พารามิเตอร์แรกเป็นประเภทพอยน์เตอร์อินเทอร์เฟซเฉพาะ พารามิเตอร์ที่สองเป็นประเภทพอยน์เตอร์การใช้งานเฉพาะ

go
func Bind(iface, to interface{}) Binding
go
var Set = wire.NewSet(
    provideMyFooer,
    wire.Bind(new(Fooer), new(*MyFooer)),
    provideBar)

การผูกค่า

เมื่อใช้ wire.Build สามารถไม่ต้องให้ provider ให้ค่า แต่ใช้ wire.Value เพื่อให้ค่าเฉพาะ wire.Value รองรับนิพจน์เพื่อสร้างค่า นิพจน์นี้จะถูกคัดลอกไปยัง injector ขณะสร้างโค้ด ดังนี้

go
type Foo struct {
    X int
}

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

injector ที่สร้าง

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

var (
    _wireFooValue = Foo{X: 42}
)

หากต้องการผูกค่าประเภทอินเทอร์เฟซ สามารถใช้ wire.InterfaceValue

go
func injectReader() io.Reader {
    wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
    return nil
}

การสร้างโครงสร้าง

ใน providerset สามารถใช้ wire.Struct เพื่อใช้ค่าส่งกลับของ provider อื่นสร้างโครงสร้างประเภทที่กำหนด

พารามิเตอร์แรกควรส่งประเภทพอยน์เตอร์โครงสร้าง ต่อมาคือชื่อฟิลด์

go
func Struct(structType interface{}, fieldNames ...string) StructProvider

ตัวอย่างดังนี้

go
type Foo int
type Bar int

func ProvideFoo() Foo {/* ... */}

func ProvideBar() Bar {/* ... */}

type FooBar struct {
    MyFoo Foo
    MyBar Bar
}

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

func injectFooBar() FoodBar {
    wire.Build(Set)
}

injector ที่สร้างอาจดังนี้

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

หากต้องการเติมทุกฟิลด์ สามารถใช้ * เช่น

go
wire.Struct(new(FooBar), "*")

ค่าเริ่มต้นคือสร้างประเภทโครงสร้าง หากต้องการสร้างประเภทพอยน์เตอร์ สามารถแก้ไขค่าส่งกลับของลายเซ็น injector

func injectFooBar() *FoodBar {
    wire.Build(Set)
}

หากต้องการละเว้นฟิลด์ สามารถเพิ่ม tag ดังนี้

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

Cleanup

หากค่าที่ provider สร้างต้องการงาน收尾หลังใช้งาน (เช่น ปิดไฟล์) provider สามารถส่งคืน closure เพื่อดำเนินการดังกล่าว injector จะไม่เรียกฟังก์ชัน cleanup นี้ เวลาเรียกเฉพาะให้ผู้เรียก injector กำหนด ดังนี้

go
type Data struct {
  // TODO wrapped database client
}

// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
  cleanup := func() {
    log.NewHelper(logger).Info("closing the data resources")
  }
  return &Data{}, cleanup, nil
}

โค้ดที่สร้างจริงอาจดังนี้

go
func wireApp(confData *conf.Data, logger log.Logger) (func(), error) {
    dataData, cleanup, err := data.NewData(confData, logger)
    if err != nil {
       return nil, nil, err
    }
    // inject data
    // ...
    return app, func() {
       cleanup()
    }, nil
}

ประเภทซ้ำกัน

พารามิเตอร์เข้าของ provider ควรไม่ซ้ำกัน โดยเฉพาะสำหรับประเภทพื้นฐาน

go
type FooBar struct {
  foo string
  bar string
}

func NewFooBar(foo string, bar string) FooBar {
  return FooBar{
      foo: foo,
      bar: bar,
  }
}

func InitializeFooBar(a string, b string) FooBar {
    panic(wire.Build(NewFooBar))
}

กรณีนี้สร้างโค้ดจะเกิดข้อผิดพลาด

provider has multiple parameters of type string

wire จะไม่สามารถแยกแยะว่าพารามิเตอร์เหล่านี้ควร inject อย่างไร เพื่อหลีกเลี่ยงความขัดแย้ง สามารถใช้นามแฝงประเภท

Golang by www.golangdev.cn edit