Wire
wire هو أداة حقن تبعية مفتوحة المصدر من جوجل، مفهوم حقن التبعية شائع جدًا في إطار عمل Spring للغة Java، وهناك أيضًا بعض مكتبات حقن التبعية في Go، مثل dig مفتوحة المصدر من Uber. لكن فلسفة حقن التبعية في wire لا تعتمد على آلية الانعكاس في اللغة، بشكل دقيق، wire هو في الواقع مولد كود، وفلسفة حقن التبعية تظهر فقط في الاستخدام، وإذا كانت هناك مشاكل، يمكن اكتشافها أثناء توليد الكود.
عنوان المستودع: 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تثبيت اعتماد الكود المصدري
go get github.com/google/wireالبدء
حقن التبعية في wire يعتمد على عنصرين، provider و injector.
provider يمكن للمطور توفير دالة بناء، كما يلي، يجب أن يكون 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
}يمكن أيضًا تجميع provider
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
}الكود المولد ليس له أي اعتماد على wire تقريبًا، ويمكنه العمل بشكل طبيعي بدون wire، وبعد ذلك عند تنفيذ go generate يمكن توليده مرة أخرى، ثم يقوم المطور باستدعاء injector الفعلي المولد وتمرير المعاملات المناسبة لإكمال حقن التبعية. أليس الكود خلال العملية بسيطًا جدًا؟ يبدو كما لو أنك توفر بضع دوال بناء، ثم تولد دالة تستدعي دوال البناء، وأخيرًا تستدعي هذه الدالة وتمرر المعاملات، وكأنك لم تفعل شيئًا معقدًا بشكل خاص، وكتابته يدويًا ممكنة أيضًا، صحيح هذا هو الحال، wire يقوم بهذه المهمة البسيطة، فقط تحولت من كتابة يدوية إلى توليد تلقائي. وفقًا لفلسفة wire، حقن التبعية يجب أن يكون أمرًا بسيطًا بهذا الشكل، ولا يجب تعقيده.
مثال
فيما يلي مثال لتعميق الفهم، هذا مثال لتهيئة تطبيق.
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 المولد.
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هذه حالة استخدام بسيطة جدًا.
الاستخدام المتقدم
ربط الواجهات
أحيانًا، عند حقن التبعية، يتم حقن تنفيذ محدد في واجهة. 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()
}معامل provider provideBar هو نوع واجهة، وهو في الواقع *MyFooer، لكي يتمكن provider من المطابقة الصحيحة عند توليد الكود، يمكننا ربط النوعين، كالتالي
المعامل الأول هو نوع مؤشر الواجهة المحدد، والثاني هو نوع مؤشر التنفيذ المحدد.
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)ربط القيم
عند استخدام wire.Build، يمكن استخدام wire.Value لتوفير قيمة محددة بدلاً من provider. 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
}التنظيف
إذا كانت القيمة التي يبنيها provider تحتاج إلى عمل تنظيف بعد الاستخدام (مثل إغلاق ملف)، يمكن لـ provider إرجاع إغلاق للقيام بهذه العملية، injector لن يستدعي دالة التنظيف هذه، متى يتم الاستدعاء يُترك لمستدعي 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 لن يتمكن من التمييز بين كيفية حقن هذه المعاملات، لتجنب التعارض، يمكن استخدام أسماء مستعارة للأنواع.
