skip to content
Kittichanr Blog

Go google wire

/ 3 min read

Intro

Google wire เป็น package ที่ช่วยจัดการเรื่องของ Dependencies injection (DI) ในภาษา go ซึ่งพัฒนาโดย google หน้าที่ของมันคือจะทำการ generate การทำงานฟังก์ชั่น ทั้งหมดของเราที่มีการ inject ค่าไปยังแต่ละฟังก์ชั่นให้เรียกใช้งานร่วมกัน

Dependencies injection (DI) คือ การนำ dependency ที่อยู่ข้างนอกเข้าไปข้างในฟังก์ชั่นแทนที่สร้างใหม่ในฟังก์ชั่น ประโยชน์คือ

  1. ลดการผูกมัดของโค้ด (loose coupling)
  2. เขียน test ง่ายขึ้น เมื่อโค้ดไม่ได้ผูกมัดกัน ทำให้เรา mock dependencies แยกออกมาแล้ว inject เข้าไป test ได้ง่าย

ตัวอย่าง

package main

import "fmt"

type Animal interface {
    MakeSound()
}

// Dog
type Dog struct{}

func (d *Dog) MakeSound() {
    fmt.Println("Woof!")
}

// Cat
type Cat struct{}

func (c *Cat) MakeSound() {
    fmt.Println("Meow!")
}

type Zoo struct {
    animals []Animal
}

func (z *Zoo) MakeSounds() {
    for _, animal := range z.animals {
        animal.MakeSound()
    }
}

// NewZoo is a constructor function for Zoo with DI
func NewZoo(animals ...Animal) *Zoo {
    return &Zoo{animals: animals}
}

func main() {
    // Injecting dependencies
    dog := &Dog{}
    cat := &Cat{}
    zoo := NewZoo(dog, cat) // dog, cat inject to NewZoo

    zoo.MakeSounds()
}

2 core concepts

1. Providers เป็นฟังก์ชั่นที่ return เพื่อสร้าง object ใหม่ เช่น

type Message string

func NewMessage() Message {
    return Message("Hi there!")
}

สามารถทำเป็น group จะเรียกว่า provider sets ได้ เช่น

package foobarbaz

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

// Foo
type Foo struct {
    X int
}

func ProvideFoo() Foo {
    return Foo{X: 42}
}

// Bar
type Bar struct {
    X int
}

func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

// Baz
type Baz struct {
    X int
}

func ProvideBaz(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
}

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

2. Injectors เป็นฟังก์ชั่นที่รวบรวม provider ฟังก์ชั่นทั้งหมดมาทำงานรวมกันหลังจาก generate ตัวอย่าง

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

// function injectors
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.SuperSet)
    return foobarbaz.Baz{}, nil
}

เมื่อรู้จัก providers และ injectors แล้วต่อไปเป็นขั้นตอนการติดตั้งและวิธีใช้งานเบื้องต้นของ go wire

Set up and usage in basic

  1. ติดตั้ง package google wire

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

  1. สร้างฟังก์ชั่น provider ที่ต้องการ เช่น
// greeter/greeter.go
package greeter

type Message string

// provider function
func NewMessage() Message {
    return Message("Hi there!")
}

type Greeter struct {
    Message Message
}

func (g Greeter) Greet() Message {
    return g.Message
}

// provider function
func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

type Event struct {
     Greeter Greeter
}

func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}

var MainBindingSet = wire.NewSet(NewMessage, NewGreeter, NewEvent)
  1. สร้างฟังก์ชั้น injector ใน di/wire.go
// di/wire.go
//go:build wireinject
// +build wireinject

package di

import (
 "context"

 "github.com/google/wire"
 "github.com/kittichanr/go-wire/greeter"
)

func InitializeApplication(ctx context.Context) (greeter.Event, func(), error) {
 wire.Build(greeter.MainBindingSet)
 return greeter.Event{}, func() {}, nil
}

จำเป็นต้องมี comment go:build และ +build เพื่อเวลารัน cli ของ wire แล้วมันมา generate จากฟังก์ชั่นใน file นี้

// go:build wireinject
// +build wireinject
  1. ใน main.go เรียก function injector มาใช้เพื่อ init ฟังก์ชั่นตอน run application
// main.go
package main

import (
 "context"
 "log"

 "github.com/kittichanr/go-wire/di"
)

func main() {
 ctx := context.Background()
 _, cleanUpFn, err := di.InitializeApplication(ctx)
 defer cleanUpFn()

 if err != nil {
  log.Fatal("initial app failed")
  panic(err)
 }
}
  1. เรียกคำสั่ง wire ./di ที่ terminal เพื่อ generate function provider ทั้งหมดที่อยู่ใน injector มาทำงานร่วมกันจะได้ไฟล์ di/wire_gen.go มีหน้าตาประมาณนี้
// Code generated by Wire. DO NOT EDIT.

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

package di

import (
 "context"
 "github.com/kittichanr/go-wire/greeter"
)

// Injectors from wire.go:

func InitializeApplication(ctx context.Context) (greeter.Event, func(), error) {
 message := greeter.NewMessage()
 greeterGreeter := greeter.NewGreeter(message)
 event := greeter.NewEvent(greeterGreeter)
 return event, func() {
 }, nil
}

Note: ไฟล์ที่ generate อันนี้ ห้ามแก้ ต้องไปแก้ที่ฟังก์ชั่น provider และเรียกคำสั่ง wire อีกทีเพื่อ generate ใหม่

สังเกตว่าไฟล์ wire_gen.go จะ inject function provider ทั้งหมดไว้ให้โดยที่เราไม่ต้องมา inject เอง เย้!!

Conclusion

go wire เป็น tool ที่ช่วยในการ generate code ให้ function ที่เป็น dependencies injection ให้สามารถทำงานร่วมกันได้ทุกฟังก์ชั่น ทำให้เราไม่ต้องเรียกทุกฟังก์ชั่นมา inject เอง และมั่นใจได้ว่าฟังก์ชั่นที่ inject ทุกอันจะทำงานเรียงตามลำดับอย่างถูกต้อง ซึ่งในตัวอย่างเป็นแค่การทำ go wire เบื้องต้นสามารถไปดูเพิ่มเติมได้ที่ go wire

source code example: https://github.com/kittichanr/go-wire-basic

Reference