Перейти на ежедневную библиотечную проводку

Go

Введение

предыдущий постЕжедневно копайтесь в библиотекеПредставлена ​​платформа внедрения зависимостей с открытым исходным кодом Uber.dig. Прочитав эту статью,@overtalkРекомендуется открытый исходный код Googlewireинструмент. Поэтому сегодняшняя статья здесь, спасибо за рекомендацию 👍

wireЭто инструмент внедрения зависимостей с открытым исходным кодом от Google. Это генератор кода, а не фреймворк. Нам просто нужен специальныйgoдокумент говоритwireЗависимости между типами, он автоматически сгенерирует для нас код, поможет нам создать объекты указанного типа и собрать его зависимости.

быстрый в использовании

Сначала установите инструменты:

$ go get github.com/google/wire/cmd/wire

Вышеупомянутая команда будет в$GOPATH/binсоздать исполняемую программу вwire, который является генератором кода. Я привык к$GOPATH/binДобавьте переменные системной среды$PATH, так что его можно запустить прямо из командной строкиwireЗаказ.

Давайте посмотрим, как это использовать на примереwire.

Теперь мы приходим в темный мир со злым монстром. Мы представляем его следующей структурой при написании метода create:

type Monster struct {
  Name string
}

func NewMonster() Monster {
  return Monster{Name: "kitty"}
}

Если есть монстры, должны быть и воины.Структура такая, а так же имеет метод создания:

type Player struct {
  Name string
}

func NewPlayer(name string) Player {
  return Player{Name: name}
}

Наконец однажды воин выполнил свою миссию и победил монстра:

type Mission struct {
  Player  Player
  Monster Monster
}

func NewMission(p Player, m Monster) Mission {
  return Mission{p, m}
}

func (m Mission) Start() {
  fmt.Printf("%s defeats %s, world peace!\n", m.Player.Name, m.Monster.Name)
}

Это может быть сцена в игре, давайте посмотрим, как собрать приведенную выше структуру и поместить ее в приложение:

func main() {
  monster := NewMonster()
  player := NewPlayer("dj")
  mission := NewMission(player, monster)

  mission.Start()
}

Когда объем кода невелик, а структура несложна, приведенная выше реализация действительно не представляет проблемы. Но когда проект достаточно большой и отношения между структурами становятся очень сложными, такой способ ручного создания каждой зависимости и последующей их сборки становится чрезвычайно громоздким и подверженным ошибкам. На этот раз воиныwireПоявившийся!

wireТребования очень простые, создайте новыйwire.goфайл (имя файла может быть произвольным), создадим нашу функцию инициализации. Например, мы хотим создать и инициализироватьMissionобъект, мы можем сделать это:

//+build wireinject

package main

import "github.com/google/wire"

func InitMission(name string) Mission {
  wire.Build(NewMonster, NewPlayer, NewMission)
  return Mission{}
}

Прежде всего, возвращаемое значение этой функции — это тип объекта, который нам нужно создать,wireПросто нужно знать тип,returnНеважно, что вы вернетесь после. Затем в функции мы вызываемwire.Build()будет создаватьMissionВ него передается конструктор типа, от которого он зависит. Например, вам нужно позвонитьNewMission()СоздайтеMissionтип,NewMission()принимает два параметра одинMonsterнаберите "АPlayerтип.MonsterТип объект должен быть вызванNewMonster()Создайте,PlayerТип объект должен быть вызванNewPlayer()Создайте. такNewMonster()иNewPlayer()Нам тоже нужно пройтиwire.

После того, как файл будет записан, выполнитеwireЗаказ:

$ wire
wire: github.com/darjun/go-daily-lib/wire/get-started/after: \
wrote D:\code\golang\src\github.com\darjun\go-daily-lib\wire\get-started\after\wire_gen.go

Смотрим на сгенерированныйwire_gen.goдокумент:

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitMission(name string) Mission {
  player := NewPlayer(name)
  monster := NewMonster()
  mission := NewMission(player, monster)
  return mission
}

этоInitMission()функция не с намиmain.goКод написан таким же, как волосы! Далее мы можем напрямуюmain.goперечислитьInitMission():

func main() {
  mission := InitMission("dj")

  mission.Start()
}

Осторожная детская обувь, возможно, нашла,wire.goиwire_gen.goПозиция заголовка файла имеет+build, но один, за которым следуетwireinject, другой!wireinject.+buildПо сути, это особенность языка Go. Условная компиляция, аналогичная C/C++, при выполненииgo buildВы можете передать некоторые параметры, в соответствии с этим параметром, чтобы решить, следует ли компилировать определенные файлы.wireИнструмент работает только сwireinjectфайл, поэтому нашwire.goфайл, чтобы добавить это. Сгенерированоwire_gen.goдля нас использовать,wireНикакой обработки не требуется, поэтому есть!wireinject.

Так как теперь есть два файла, мы не можем использоватьgo run main.goДля запуска программы вы можете использоватьgo run .бегать. Результат точно такой же, как и в предыдущем примере!

Обратите внимание, что если вы бежите,InitMissionпереопределить, а затем проверить//+build wireinjectиpackage mainЕсли между этими двумя строками есть пустая строка, эта пустая строка должна присутствовать! ВидетьGitHub.com/Google/wire…. Тот, кого завербовали, молча ударил 1 в моем сердце, хорошо?

Базовые концепты

wireЕсть два основных понятия,Provider(конструктор) иInjector(инжектор).ProviderПо сути, это создать функцию, это всем понятно. над намиInitMissionэтоInjector. Каждый инжектор на самом деле является функцией создания и инициализации объекта. В этой функции нам просто нужно сказатьwireкакой тип объекта создать, этот тип зависимости,wireИнструмент сгенерирует для нас функцию для завершения создания и инициализации объекта.

параметр

Столь же внимательный, вы должны были обнаружить, что мы написали вышеInitMission()функция сstringтип параметра. и генерируетсяInitMission()В функции этот параметр передаетсяNewPlayer().NewPlayer()необходимостьstringпараметр типа, а тип параметраstring. так сгенерированоInitMission()функция, этот параметр передается вNewPlayer(). если мы позволимNewMonster()также принятьstringА параметры?

func NewMonster(name string) Monster {
  return Monster{Name: name}
}

затем генерируетсяInitMission()в функцииNewPlayer()иNewMonster()получит этот параметр:

func InitMission(name string) Mission {
  player := NewPlayer(name)
  monster := NewMonster(name)
  mission := NewMission(player, monster)
  return mission
}

По факту,wireПри генерации кода параметры (или зависимости), требуемые конструктором, ищутся в параметрах или генерируются другими конструкторами. Выбор параметра или конструктора полностью зависит от типа. Если параметры или объекты, сгенерированные конструктором, имеют одинаковый тип, запуститеwireИнструмент сообщит об ошибке. Если мы хотим настроить поведение создания, нам нужно создать разные структуры параметров для разных типов:

type PlayerParam string
type MonsterParam string

func NewPlayer(name PlayerParam) Player {
  return Player{Name: string(name)}
}

func NewMonster(name MonsterParam) Monster {
  return Monster{Name: string(name)}
}

func main() {
  mission := InitMission("dj", "kitty")
  mission.Start()
}

// wire.go
func InitMission(p PlayerParam, m MonsterParam) Mission {
  wire.Build(NewPlayer, NewMonster, NewMission)
  return Mission{}
}

Сгенерированный код выглядит следующим образом:

func InitMission(m MonsterParam, p PlayerParam) Mission {
  player := NewPlayer(p)
  monster := NewMonster(m)
  mission := NewMission(player, monster)
  return mission
}

Когда параметры более сложные, рекомендуется помещать параметры в структуру.

Ошибка

Не все строительные работы могут быть успешными, возможно, воины погибнут от рук злодеев, прежде чем покинут гору:

func NewPlayer(name string) (Player, error) {
  if time.Now().Unix()%2 == 0 {
    return Player{}, errors.New("player dead")
  }
  return Player{Name: name}, nil
}

мы создаемслучайный сбой, модифицировать форсункуInitMission()подпись, добавлениеerrorвозвращаемое значение:

func InitMission(name string) (Mission, error) {
  wire.Build(NewMonster, NewPlayer, NewMission)
  return Mission{}, nil
}

Сгенерированный код будетNewPlayer()возвращенная ошибка, какInitMission()Возвращаемое значение:

func InitMission(name string) (Mission, error) {
  player, err := NewPlayer(name)
  if err != nil {
    return Mission{}, err
  }
  monster := NewMonster()
  mission := NewMission(player, monster)
  return mission, nil
}

wireследитьfail-fastПринцип, что ошибки должны быть обработаны. Если наш инжектор не возвращает ошибку, но конструктор возвращает ошибку,wireИнструмент сообщит об ошибке!

Расширенные возможности

Ниже приводится краткое введениеwireрасширенные возможности.

ProviderSet

Иногда несколько типов могут иметь одни и те же зависимости, и мы передаем один и тот же конструктор вwire.Build()Не только громоздкая, но и сложная в обслуживании, зависимость модифицируется, все входящиеwire.Build()место для модификации. к этому концу,wireпредоставилProviderSet(коллекция конструкторов), вы можете упаковать несколько конструкторов в коллекцию, и вам нужно будет использовать эту коллекцию позже. Предположим, наша история о воинах и монстрах имеет две концовки:

type EndingA struct {
  Player  Player
  Monster Monster
}

func NewEndingA(p Player, m Monster) EndingA {
  return EndingA{p, m}
}

func (p EndingA) Appear() {
  fmt.Printf("%s defeats %s, world peace!\n", p.Player.Name, p.Monster.Name)
}

type EndingB struct {
  Player  Player
  Monster Monster
}

func NewEndingB(p Player, m Monster) EndingB {
  return EndingB{p, m}
}

func (p EndingB) Appear() {
  fmt.Printf("%s defeats %s, but become monster, world darker!\n", p.Player.Name, p.Monster.Name)
}

Напишите два инжектора:

func InitEndingA(name string) EndingA {
  wire.Build(NewMonster, NewPlayer, NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(NewMonster, NewPlayer, NewEndingB)
  return EndingB{}
}

Наблюдаем два звонкаwire.Build()нужно пройти вNewMonsterиNewPlayer. Два — это нормально, если их слишком много, писать будет проблематично, а изменить — непросто. В этом случае мы можем сначала определитьProviderSet:

var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

Используйте это напрямуюset:

func InitEndingA(name string) EndingA {
  wire.Build(monsterPlayerSet, NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(monsterPlayerSet, NewEndingB)
  return EndingB{}
}

Затем, если вы хотите добавить или удалить конструктор, измените его напрямую.setможно определить.

конструктор конструкций

потому что нашEndingAиEndingBПоля толькоPlayerиMonster, нам не нужно явно указывать для них конструктор, мы можем использовать его напрямуюwireкоторый предоставилконструктор конструкций(Поставщик структуры). Конструктор структуры создает структуру некоторого типа, а затем заполняет ее поля параметрами или вызывает другие конструкторы. Например, в приведенном выше примере мы удаляемNewEndingA()иNewEndingB(), а затем предоставить для них конструкторы структур:

var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "Player", "Monster"))
var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "Player", "Monster"))

func InitEndingA(name string) EndingA {
  wire.Build(endingASet)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(endingBSet)
  return EndingB{}
}

Использование конструктора структурыwire.Structинъекции, первый параметр фиксируется какnew(结构名), за которым следует любое количество параметров, указывающих, в какие поля структуры необходимо ввести значения. Сверху нам нужно ввестиPlayerиMonsterдва поля. Или мы также можем использовать подстановочные знаки*Указывает на вставку всех полей:

var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "*"))
var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "*"))

wireГенерирует правильный код для нас, и это здорово:

func InitEndingA(name string) EndingA {
  player := NewPlayer(name)
  monster := NewMonster()
  endingA := EndingA{
    Player:  player,
    Monster: monster,
  }
  return endingA
}

значение привязки

Иногда нам нужно привязать значение к типу, не полагаясь на конструктор для создания нового значения каждый раз. Некоторые типы по своей сути являются синглтонами, например конфигурация, объекты базы данных (sql.DB). В этот момент мы можем использоватьwire.Valueпривязать значение, использоватьwire.InterfaceValueПривязать интерфейс. Например, наш монстр всегда былKitty, нам не нужно создавать его каждый раз, можно напрямую привязать это значение:

var kitty = Monster{Name: "kitty"}

func InitEndingA(name string) EndingA {
  wire.Build(NewPlayer, wire.Value(kitty), NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(NewPlayer, wire.Value(kitty), NewEndingB)
  return EndingB{}
}

Обратите внимание, что это значение будет копироваться каждый раз, когда оно используется, вам нужно убедиться, что копия не имеет побочных эффектов:

// wire_gen.go
func InitEndingA(name string) EndingA {
  player := NewPlayer(name)
  monster := _wireMonsterValue
  endingA := NewEndingA(player, monster)
  return endingA
}

var (
  _wireMonsterValue = kitty
)

Структурные поля как конструкторы

Иногда мы пишем конструктор, который просто возвращает поле структуры, тогда мы можем использоватьwire.FieldsOfУпрощенная операция. Теперь мы непосредственно создаемMissionструктуру, если вы хотите получитьMonsterиPlayerтип объекта, вы можетеMissionиспользоватьwire.FieldsOf:

func NewMission() Mission {
  p := Player{Name: "dj"}
  m := Monster{Name: "kitty"}

  return Mission{p, m}
}

// wire.go
func InitPlayer() Player {
  wire.Build(NewMission, wire.FieldsOf(new(Mission), "Player"))
}

func InitMonster() Monster {
  wire.Build(NewMission, wire.FieldsOf(new(Mission), "Monster"))
}

// main.go
func main() {
  p := InitPlayer()
  fmt.Println(p.Name)
}

Аналогично, первый параметрnew(结构名), за которым следует несколько параметров, указывающих, какие поля использовать в качестве конструкторов,*значит все.

функция очистки

Конструктор может предоставить функцию очистки. Если последующий конструктор не вернет значение, будет вызвана функция очистки, возвращенная предыдущим конструктором:

func NewPlayer(name string) (Player, func(), error) {
  cleanup := func() {
    fmt.Println("cleanup!")
  }
  if time.Now().Unix()%2 == 0 {
    return Player{}, cleanup, errors.New("player dead")
  }
  return Player{Name: name}, cleanup, nil
}

func main() {
  mission, cleanup, err := InitMission("dj")
  if err != nil {
    log.Fatal(err)
  }

  mission.Start()
  cleanup()
}

// wire.go
func InitMission(name string) (Mission, func(), error) {
  wire.Build(NewMonster, NewPlayer, NewMission)
  return Mission{}, nil, nil
}

некоторые детали

Во-первых, мы звонимwireгенерироватьwire_gen.goПосле этого, еслиwire.goФайл был изменен, просто нужно выполнитьgo generateВот и все.go generateОчень удобно, пока не написал статьюgenerate, можете посмотреть, если интересноГлубокое понимание генерации Go.

Суммировать

wireэто сgo-cloudпримерguestbookОпубликовано вместе, вы можете прочитатьguestbookпосмотреть, как это используетсяwireиз. иdigразные,wireпросто сгенерируйте код, не используйтеreflectБиблиотека, производительность не о чем беспокоиться. Потому что код, который он генерирует, в основном такой же, как и тот, который вы написали сами. Если сгенерированный код имеет проблемы с производительностью, велика вероятность, что вы напишете его сами.

Если вы найдете забавную и простую в использовании языковую библиотеку Go, вы можете отправить сообщение о проблеме в ежедневной библиотеке Go GitHub😄

Ссылаться на

  1. провод GitHub:github.com/google/wire
  2. Перейти на ежедневный репозиторий GitHub:GitHub.com/Darenjun/go-of…

я

мой блог

Добро пожаловать, чтобы обратить внимание на мою общедоступную учетную запись WeChat [GoUpUp], учитесь вместе и добивайтесь прогресса вместе ~

Эта статья опубликована в блогеOpenWriteвыпуск!