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 install github.com/google/wire/cmd/wire@latestติดตั้ง dependency ซอร์สโค้ด
go get github.com/google/wireการเริ่มต้น
ใน wire การ inject dependency ขึ้นอยู่กับสององค์ประกอบ provider และ injector
provider สามารถเป็น constructor ที่ผู้พัฒนาให้ไว้ ดังนี้ Provider ต้องเปิดเผยภายนอก
package foobarbaz
type Foo struct {
X int
}
// สร้าง Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}มีพารามิเตอร์
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}หรือมีพารามิเตอร์และค่าส่งกลับ
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 เข้าด้วยกัน
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 เพื่อประกาศ คำประกาศนี้ไม่ควรถูกเรียก ไม่ควรถูกคอมไพล์
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +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 เนื้อหาดังนี้
// 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
var ServerProviderSet = wire.NewSet(NewHttpserver)
type HttpServer struct {
net.Addr
}
func NewHttpserver(addr net.Addr) (*HttpServer, error) {
return &HttpServer{addr}, nil
}MysqlClient และ System provider ด้านล่างนี้ก็เช่นกัน
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: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 นี้ขณะคอมไพล์ จากนั้นดำเนินการคำสั่งต่อไปนี้ หากมีผลลัพธ์ดังนี้แสดงว่าสร้างสำเร็จ
$ wire
$ wire: golearn: wrote /golearn/wire_gen.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
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 จับคู่ตามประเภท
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 สามารถจับคู่ได้อย่างถูกต้อง เราสามารถผูกสองประเภทเข้าด้วยกัน ดังนี้
พารามิเตอร์แรกเป็นประเภทพอยน์เตอร์อินเทอร์เฟซเฉพาะ พารามิเตอร์ที่สองเป็นประเภทพอยน์เตอร์การใช้งานเฉพาะ
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)การผูกค่า
เมื่อใช้ wire.Build สามารถไม่ต้องให้ provider ให้ค่า แต่ใช้ wire.Value เพื่อให้ค่าเฉพาะ wire.Value รองรับนิพจน์เพื่อสร้างค่า นิพจน์นี้จะถูกคัดลอกไปยัง injector ขณะสร้างโค้ด ดังนี้
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
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}การสร้างโครงสร้าง
ใน providerset สามารถใช้ wire.Struct เพื่อใช้ค่าส่งกลับของ provider อื่นสร้างโครงสร้างประเภทที่กำหนด
พารามิเตอร์แรกควรส่งประเภทพอยน์เตอร์โครงสร้าง ต่อมาคือชื่อฟิลด์
func Struct(structType interface{}, fieldNames ...string) StructProviderตัวอย่างดังนี้
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 ที่สร้างอาจดังนี้
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}หากต้องการเติมทุกฟิลด์ สามารถใช้ * เช่น
wire.Struct(new(FooBar), "*")ค่าเริ่มต้นคือสร้างประเภทโครงสร้าง หากต้องการสร้างประเภทพอยน์เตอร์ สามารถแก้ไขค่าส่งกลับของลายเซ็น injector
func injectFooBar() *FoodBar {
wire.Build(Set)
}หากต้องการละเว้นฟิลด์ สามารถเพิ่ม tag ดังนี้
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}Cleanup
หากค่าที่ provider สร้างต้องการงาน收尾หลังใช้งาน (เช่น ปิดไฟล์) provider สามารถส่งคืน closure เพื่อดำเนินการดังกล่าว injector จะไม่เรียกฟังก์ชัน cleanup นี้ เวลาเรียกเฉพาะให้ผู้เรียก injector กำหนด ดังนี้
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
}โค้ดที่สร้างจริงอาจดังนี้
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 ควรไม่ซ้ำกัน โดยเฉพาะสำหรับประเภทพื้นฐาน
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 stringwire จะไม่สามารถแยกแยะว่าพารามิเตอร์เหล่านี้ควร inject อย่างไร เพื่อหลีกเลี่ยงความขัดแย้ง สามารถใช้นามแฝงประเภท
