Wire
wire là một công cụ dependency injection được Google mã nguồn mở, khái niệm dependency injection này khá phổ biến trong framework Spring của Java, Go cũng có một số thư viện dependency injection như dig được Uber mã nguồn mở. Tuy nhiên triết lý dependency injection của wire không dựa trên cơ chế reflection của ngôn ngữ, nói một cách nghiêm ngặt, wire thực ra là một trình sinh mã, triết lý dependency injection chỉ thể hiện ở cách sử dụng, nếu có vấn đề thì có thể phát hiện ra trong quá trình sinh mã.
Địa chỉ kho lưu trữ: google/wire: Compile-time Dependency Injection for Go (github.com)
Địa chỉ tài liệu: wire/docs/guide.md at main · google/wire (github.com)
Cài đặt
Cài đặt công cụ sinh mã
go install github.com/google/wire/cmd/wire@latestCài đặt phụ thuộc mã nguồn
go get github.com/google/wireNhập môn
Dependency injection trong wire dựa trên hai phần tử, provider và injector.
provider có thể là một constructor do nhà phát triển cung cấp, như dưới đây, Provider phải được công khai.
package foobarbaz
type Foo struct {
X int
}
// Construct Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}Với tham số
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}Cũng có thể có tham số và giá trị trả về
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
}Cũng có thể kết hợp các provider
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)TIP
wire có quy định sau đối với giá trị trả về của provider
- Giá trị trả về đầu tiên là giá trị do provider cung cấp
- Giá trị trả về thứ hai phải là
func() | error - Giá trị trả về thứ ba, nếu giá trị trả về thứ hai là
func, thì giá trị trả về thứ ba phải làerror
injector là một hàm do wire sinh ra, nó chịu trách nhiệm gọi provider theo thứ tự chỉ định, chữ ký của injector do nhà phát triển định nghĩa, wire sinh phần thân hàm cụ thể, khai báo thông qua việc gọi wire.Build, khai báo này không nên được gọi, càng không nên được biên dịch.
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 được định nghĩa
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}Sau đó thực thi
wiresẽ sinh ra wire_gen.go, nội dung như sau
// 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 thực tế được sinh ra
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
}Mã được sinh ra hầu như không có phụ thuộc nào vào wire, có thể làm việc bình thường mà không cần wire, và có thể sinh lại sau đó bằng cách thực thi go generate, sau đó, nhà phát triển gọi injector thực tế được sinh ra và truyền tham số tương ứng để hoàn thành dependency injection. Có phải toàn bộ quá trình code khá đơn giản, cảm giác như chỉ cần cung cấp vài constructor, sau đó sinh ra một hàm gọi constructor, cuối cùng gọi hàm này truyền tham số, dường như cũng không làm việc gì đặc biệt phức tạp, viết tay cũng có thể được, đúng vậy, wire chính là làm một việc đơn giản như vậy, chỉ là từ viết tay biến thành tự động sinh. Theo triết lý của wire, dependency injection vốn dĩ nên là một việc đơn giản như vậy, không nên phức tạp hóa.
Ví dụ
Dưới đây là một ví dụ để hiểu sâu hơn, đây là một ví dụ khởi tạo app.
HttpServer provider nhận một tham số net.Addr, trả về con trỏ và error
var ServerProviderSet = wire.NewSet(NewHttpserver)
type HttpServer struct {
net.Addr
}
func NewHttpserver(addr net.Addr) (*HttpServer, error) {
return &HttpServer{addr}, nil
}MysqlClient và System provider dưới đây tương tự
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
}Sau khi định nghĩa provider, cần định nghĩa injector, tốt nhất nên tạo một file wire.go để định nghĩa
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"net"
)
// Định nghĩa injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
// Gọi provider theo thứ tự
panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}+build wireinject để bỏ qua injector này khi biên dịch. Sau đó thực thi lệnh sau, nếu có đầu ra như sau nghĩa là đã sinh thành công.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.goCode sau khi sinh như sau
// 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:
// Định nghĩa 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
}Có thể thấy logic rất rõ ràng, thứ tự gọi cũng chính xác, cuối cùng thông qua injector được sinh ra để khởi động 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()
}Đầu ra cuối cùng như sau
2023/08/01 19:20:48 app run on 127.0.0.1:8080Đây là một ví dụ sử dụng rất đơn giản.
Cách sử dụng nâng cao
Ràng buộc interface
Đôi khi, dependency injection sẽ inject một implementation cụ thể vào interface. wire khi dependency injection dựa trên khớp kiểu.
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()
}Tham số của provider provideBar là một kiểu interface, thực tế nó là *MyFooer, để code khi sinh ra provider có thể khớp chính xác, chúng ta có thể ràng buộc hai kiểu, như sau
Tham số đầu tiên là kiểu con trỏ interface cụ thể, tham số thứ hai là kiểu con trỏ implementation cụ thể.
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)Ràng buộc giá trị
Khi sử dụng wire.Build, có thể không cần provider cung cấp giá trị, cũng có thể sử dụng wire.Value để cung cấp một giá trị cụ thể. wire.Value hỗ trợ biểu thức để构造 giá trị, biểu thức này sẽ được sao chép vào injector khi sinh code, như sau.
type Foo struct {
X int
}
func injectFoo() Foo {
wire.Build(wire.Value(Foo{X: 42}))
return Foo{}
}injector được sinh ra
func injectFoo() Foo {
foo := _wireFooValue
return foo
}
var (
_wireFooValue = Foo{X: 42}
)Nếu muốn ràng buộc một giá trị kiểu interface, có thể sử dụng wire.InterfaceValue
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}Construct struct
Trong providerset, có thể sử dụng wire.Struct để sử dụng giá trị trả về của provider khác构建 một struct kiểu chỉ định.
Tham số đầu tiên nên truyền kiểu con trỏ struct, sau đó là tên các trường.
func Struct(structType interface{}, fieldNames ...string) StructProviderVí dụ như sau
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 được sinh ra có thể như sau
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}Nếu muốn điền tất cả các trường, có thể sử dụng *, ví dụ
wire.Struct(new(FooBar), "*")Mặc định là construct kiểu struct, nếu muốn construct kiểu con trỏ, có thể sửa giá trị trả về của chữ ký injector
func injectFooBar() *FoodBar {
wire.Build(Set)
}Nếu muốn bỏ qua trường, có thể thêm tag, như sau
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}Cleanup
Nếu provider construct một giá trị cần thực hiện công việc收尾 sau khi sử dụng (ví dụ đóng một file), provider có thể trả về một closure để thực hiện thao tác như vậy, injector sẽ không gọi hàm cleanup này, cụ thể khi nào gọi do người gọi injector quyết định, như sau.
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
}Code thực tế được sinh ra có thể như sau
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
}Trùng kiểu
Tham số đầu vào của provider tốt nhất không nên trùng kiểu, đặc biệt là đối với một số kiểu cơ bản
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))
}Trường hợp này khi sinh code sẽ báo lỗi
provider has multiple parameters of type stringwire sẽ không thể phân biệt các tham số này nên được inject như thế nào, để tránh xung đột, có thể sử dụng bí danh kiểu.
Best Practices
- Tổ chức code rõ ràng: Tách biệt provider và injector vào các file riêng
- Sử dụng wireinject tag: Luôn sử dụng
//go:build wireinjectcho file định nghĩa injector - Kiểm tra lỗi: Wire sẽ báo lỗi tại thời điểm biên dịch nếu có vấn đề về dependency
- Tránh trùng kiểu: Không sử dụng nhiều tham số cùng kiểu cơ bản trong provider
- Sử dụng interface binding: Sử dụng wire.Bind khi cần inject vào interface
Kết luận
Wire là một công cụ dependency injection độc đáo cho Go, với phương pháp sinh code tại thời điểm biên dịch thay vì sử dụng reflection. Điều này giúp phát hiện lỗi sớm và tạo ra code hiệu quả hơn. Mặc dù có vẻ phức tạp ban đầu, wire thực sự đơn giản hóa việc quản lý dependency trong các dự án Go lớn.
