Skip to content

Wire

wire Google tarafından açık kaynak yapılan bir bağımlılık enjeksiyon aracıdır. Bağımlılık enjeksiyonu Java'nın Spring çerçevesi ile popülerleşen bir kavramdır. Go'nun da Uber'in açık kaynaklı dig'i gibi bazı bağımlılık enjeksiyon kütüphaneleri vardır. Ancak wire'ın bağımlılık enjeksiyon yaklaşımı dil yansıma mekanizmalarına dayanmaz. Katı bir şekilde söylemek gerekirse wire aslında bir kod üreticisidir ve bağımlılık enjeksiyon kavramı yalnızca kullanımında ortaya çıkar. Sorun varsa kod üretimi sırasında tespit edilebilir.

Depo: google/wire: Compile-time Dependency Injection for Go (github.com)

Dokümantasyon: wire/docs/guide.md at main · google/wire (github.com)

Kurulum

Kod üretme aracını kurun:

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

Kaynak kod bağımlılıklarını kurun:

go get github.com/google/wire

Başlangıç

wire'da bağımlılık enjeksiyonu iki öğeye dayanır: provider ve injector.

provider geliştirici tarafından sağlanan bir yapıcı fonksiyon olabilir. Aşağıda gösterildiği gibi Provider export edilmelidir:

go
package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo constructs a Foo
func ProvideFoo() Foo {
    return Foo{X: 42}
}

Parametreli:

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}
}

Parametreler ve dönüş değerleri de olabilir:

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
}

Provider'lar birleştirilebilir:

go
package foobarbaz

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

// ...

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

TIP

wire'ın provider dönüş değerleri için aşağıdaki kuralları vardır:

  • İlk dönüş değeri provider tarafından sağlanan değerdir
  • İkinci dönüş değeri func() | error olmalıdır
  • Üçüncü dönüş değeri varsa ve ikinci dönüş değeri func ise üçüncü dönüş değeri error olmalıdır

injector wire tarafından üretilen ve provider'ları belirtilen sırayla çağırmaktan sorumlu bir fonksiyondur. Injector'ın imzası geliştirici tarafından tanımlanır ve wire spesifik fonksiyon gövdesini oluşturur. Bu wire.Build çağrısı ile bildirilir ve bu bildirim çağrılmamalıdır derlenmemelidir.

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"
)

// Define injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

Sonra çalıştırın:

wire

Bu aşağıdaki içerikle wire_gen.go dosyası oluşturacaktır:

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"
)

// Actual generated 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
}

Oluşturulan kodun wire'a neredeyse hiç bağımlılığı yoktur ve wire olmadan da normal çalışabilir. Daha sonra go generate çalıştırılarak yeniden oluşturulabilir. Bundan sonra geliştiriciler ilgili parametrelerle oluşturulan injector'ı çağırarak bağımlılık enjeksiyonunu tamamlar. Tüm süreç oldukça basit değil mi? Birkaç yapıcı fonksiyon sağlamak sonra yapıcı fonksiyonları çağıran bir fonksiyon oluşturmak ve son olarak bu fonksiyonu parametrelerle çağırmak gibi görünüyor. Özellikle karmaşık bir şey yapmıyor ve elle yazılabilirdi. Evet wire tam olarak bunu yapar - bu kadar basit bir şey sadece elle yazılmak yerine otomatikleştirilir. Wire'ın felsefesine göre bağımlılık enjeksiyonu bu kadar basit bir şey olmalıdır ve karmaşık olmamalıdır.

Örnek

Anlamayı derinleştirmek için bir örnek. Bu bir uygulama başlatma örneğidir.

HttpServer provider'ı net.Addr parametresi alır ve bir işaretçi ve error döndürür:

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

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

Aşağıdaki MysqlClient ve System provider'ları benzerdir:

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'ları tanımladıktan sonra injector'ları tanımlamanız gerekir. Bunları tanımlamak için yeni bir wire.go dosyası oluşturmak en iyisidir:

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

package main

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

// Define injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  // Call providers in order
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject derleme sırasında bu injector'ı yoksaymak içindir. Sonra aşağıdaki komutu çalıştırın. Çıktı aşağıdaki gibiyse oluşturma başarılıdır:

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

Oluşturulan kod aşağıdaki gibidir:

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:

// Define 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
}

Mantiğin çok açık olduğunu ve çağrı sırasının da doğru olduğunu görebilirsiniz. Son olarak oluşturulan injector'ı çağırarak uygulamayı başlatın:

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

Son çıktı aşağıdaki gibidir:

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

Bu çok basit bir kullanım örneğidir.

İleri Düzey Kullanım

Interface Bağlama

Bazen bağımlılık enjeksiyonu sırasında somut bir implementasyon bir interface'e enjekte edilir. wire bağımlılık enjeksiyonu sırasında eşleşmeleri türe göre yapar.

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

provideBar provider'ının parametresi bir interface türüdür ancak aslında *MyFooer'dır. Provider'ın kod üretimi sırasında doğru eşleşmesini sağlamak için iki türü birbirine bağlayabiliriz:

İlk parametre somut interface işaretçi türüdür ve ikincisi somut implementasyon işaretçi türüdür:

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

Değer Bağlama

wire.Build kullanırken bir değer sağlamak için provider kullanmak zorunda değilsiniz. Somut bir değer sağlamak için wire.Value da kullanabilirsiniz. wire.Value değerleri oluşturmak için ifadeleri destekler ve bu ifade kod oluştururken injector'a kopyalanır:

go
type Foo struct {
    X int
}

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

Oluşturulan injector:

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

var (
    _wireFooValue = Foo{X: 42}
)

Bir interface türü değeri bağlamak istiyorsanız wire.InterfaceValue kullanabilirsiniz:

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

Struct Oluşturma

Bir providerset içinde diğer provider'lardan dönüş değerlerini kullanarak belirtilen türde bir struct oluşturmak için wire.Struct kullanabilirsiniz.

İlk parametre bir struct işaretçi türü olmalıdır ve ardından alan adları gelir:

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

Örnek:

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

Oluşturulan injector şöyle görünebilir:

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

Tüm alanları doldurmak istiyorsanız * kullanabilirsiniz:

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

Varsayılan olarak bir struct türü oluşturur. Bir işaretçi türü oluşturmak istiyorsanız injector imzasının dönüş değerini değiştirebilirsiniz:

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

Bir alanı yoksaymak istiyorsanız bir etiket ekleyebilirsiniz:

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

Temizleme

Bir provider tarafından oluşturulan bir değer kullanıldıktan sonra temizleme çalışması gerektiriyorsa (dosya kapatma gibi) provider bu tür işlemleri gerçekleştirmek için bir closure döndürebilir. Injector bu temizleme fonksiyonunu çağırmaz. Ne zaman çağrılacağı injector'ı çağıran kişiye bırakılır:

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
}

Gerçek oluşturulan kod şöyle görünebilir:

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
}

Tür Çoğaltması

Provider parametrelerinde özellikle temel türlerde çoğaltılmış türler olmaması en iyisidir:

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

Bu durumda kod üretimi hata verecektir:

provider has multiple parameters of type string

wire bu parametrelerin nasıl enjekte edileceğini ayırt edemeyecektir. Çakışmaları önlemek için tür takma adlarını kullanabilirsiniz.

Golang by www.golangdev.cn edit