golang常用庫包:Go依賴註入(DI)工具-wire使用

来源:https://www.cnblogs.com/jiujuan/archive/2022/04/12/16136633.html
-Advertisement-
Play Games

google 出品的依賴註入庫 wire:https://github.com/google/wire 什麼是依賴註入 依賴註入 ,英文全名是 dependency injection,簡寫為 DI。 百科解釋: 依賴註入是指程式運行過程中,如果需要調用另一個對象協助時,無須在代碼中創建被調用者,而 ...


google 出品的依賴註入庫 wire:https://github.com/google/wire

什麼是依賴註入

依賴註入 ,英文全名是 dependency injection,簡寫為 DI。

百科解釋:

依賴註入是指程式運行過程中,如果需要調用另一個對象協助時,無須在代碼中創建被調用者,而是依賴於外部的註入。

在用編程語言編寫程式時,比如用 java 語言,會編寫很多類,這些類之間相互調用,完成一個具體的功能。

例如,從 MySQL 獲取數據,那麼需要一個 MySQL 操作類 。

第一次編寫mysql操作類:

class MySQL{
}

要從 mysql 獲取數據,那麼 mysql 資料庫的用戶名,密碼,地址等等這些配置信息,也是需要的,繼續編寫 MySQL 類:

package com.demo.mysql

class MySQL {
    getMySQLConfig() {
        port = 3306;
        username = "xxx";
        password = "xxx";
    }
    
    initMySQL(){}
   
    querySQL(){}
}

進一步思考,上面的 MySQL 操作類程式有什麼不妥的地方?

編程原則里有一個原則就是:單一職責

也就是說一個類最好只乾一件事情。

根據這個原則在看看 MySQL 類,裡面有獲取資料庫配置數據,也有操作MySQL的方法,不是單一職責的。
那裡面獲取資料庫配置數據,可不可以單獨拎出來用一個類表示? 當然可以。

因為 MySQL 配置數據,多數是從文件里讀取的,上面 MySQL 類是寫死,這也是不合理的一個地方。
而配置文件的來源,可以是 yml 格式文件,也可以是 toml 格式文件,還可以是遠程文件。

第二次編寫mysql操作類:

修改上面的類,增加一個獲取資料庫配置的類:

package com.demo.mysql

class MySQLConfig {
      getMySQLConfig() {
        // 從配置文件獲取 mysql 配置數據
    }
}

獲取數據的類變成:

package com.demo.mysql

class MySQL {
    initMySQL(){
     // 獲取資料庫的配置信息
     mysqlconfig = new MySQLConfig();
    }
   
    querySQL(){}
}

思考一下,上面改寫後的類有什麼不妥的地方?
獲取mysql的配置信息,是不是要在 MySQL 類里 new一下, 實例化一下,如果不在同一個包下,還要把配置類引入進來在才能實例化。這裡能不能優化下,當然可以。

直接把資料庫配置類註入到 MySQL 操作類里。這就是依賴註入

  • 依賴是什麼?註入又是什麼?
    mysql 操作類依賴誰?依賴資料庫配置類。
    註入什麼?把資料庫配置類註入到 mysql 操作類里。
    註入是一個動作,把一個類註入到另外一個類。
    依賴是一種關係,類關係,一個類要完全發揮作用,需要依賴另外一個類。

要完成數據操作,mysql操作類是需要依賴資料庫配置類的,把資料庫配置類註入到mysql操作類里,就可以完成操作類功能。

第三次編寫mysql操作類:

偽代碼示例:

package com.demo.mysql

class MySQL {
    private MySQLConfig config
    MySQL(MySQLConfig mysqlconfig) { // 資料庫配置類這裡註入到mysql操作類里
        config = mysqlconfig
    }
    initMySQL(){
    
    }
   
    querySQL(){}
}

把資料庫配置類註入到mysql操作類里。

寫 java 的人都知道 java 框架里有一個 spring 全家桶,spring 框架包核心有2個,其中有一個核心就是 IoC,另一個是 aop。

IoC 的全稱:Inversion of Control,控制反轉。

這個控制反轉也是面向對象編程原則之一。

但是這個控制反轉比較難理解,如果結合上面的 DI 來理解,就比較容易理解點。
可以把 DI 看作是 IoC 編程原則的一個具體實現。

依賴註入還可以從另外的軟體設計思想來理解:

  1. 分離關註點
  2. 高內聚,低耦合

對資料庫 mysql 的操作和 mysql 的配置信息,這個 2 個是可以相互獨立,相分離的。

何時使用依賴註入

當你的項目規模不大,文件不是很多,一個文件調用只需要傳入少量依賴對象時,這時使用依賴註入就會使程式變得繁瑣。

當規模變大,單個對象使用需要調用多個依賴對象時,而這些依賴又有自己依賴對象,這時對象創建變得繁瑣,那麼這時候依賴註入就可以出場了。

wire 概念說明

wire 簡介

wire 是由 google 開源的一個用 Go 語言實現的依賴註入代碼生成工具。它能夠根據你寫的代碼生成相應的依賴註入 Go 代碼。

與其他依賴註入工具不同,比如 uber 的 dig 和 facebook 的 inject,這 2 個工具都是使用反射實現的依賴註入,而且是運行時註入(runtime dependency injection)。

wire 是編譯代碼時生成代碼的依賴註入,是編譯期間註入依賴代碼(compile-time dependency injection)。而且代碼生成期間,如果依賴註入有問題,生成依賴代碼時就會出錯,就可以報出問題來,而不必等到代碼運行時才暴露出問題。

provider 和 injector

首先,需要理解 wire 的 2 個核心概念:provider 和 injector。

從上面 java 模擬依賴註入的例子中,可以簡化出依賴註入的步驟:

第一:需要 New 出一個類實例
第二:把這個 New 出來的類實例通過構造函數或者其他方式“註入”到需要使用它的類中
第三:在類中使用這個 New 出來的實例

從上面步驟來理解 wire 的 2 個核心概念 provider 和 injector。

provider 就相當於上面 New 出來的類實例。
injector 就相當於“註入”動作前,把所需依賴函數進行聚合,根據這個聚合的函數生成依賴關係。

provider:提供一個對象。
injector:負責根據對象依賴關係,生成新程式。

provider

provider 是一個普通的 Go 函數 ,可以理解為是一個對象的構造函數。為下麵生成 injector 函數提供”構件“。

看下麵例子,來自 go blog

這篇 blog 是 2018.10.9 發表,可能一些信息有點老,再參考 github guide ,這篇 guide 最後更新於 2021.1.26。

下麵的 NewUserStore() 函數可以看作是一個 provider。這個函數需要傳入 *Config 和 *mysql.DB 2 個參數。

// NewUserStore 是一個 provider for *UserStore,*UserStore 依賴 *Config,*mysql.DB
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {... ...}

// NewDefaultConfig 是一個 provider for *Config,沒有任何依賴
func NewDefaultConfig() *Config {...}

// NewDB 是 *mysql.DB 的一個 provider ,依賴於資料庫連接信息 *ConnectionInfo
func NewDB(info *ConnectionInfo) (*mysql.DB, error){...}

provider 可以組合成一組 provider set。對於經常在一起使用的 providers 來說,這個非常有用。使用 wire.NewSet 方法可以把他們組合在一起,

var SuperSet = wire.NewSet(NewUserStore, NewDefaultConfig)

你也可以把其他的 provider sets 加入一個 provider set,

import (
    “example.com/some/other/pkg”
)

// ... ...
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

wire.NewSet() 函數:

這個函數可以把相關的 provider 組合在一起然後使用。當然也可以單獨使用,如 var Provider = wire.NewSet(NewDB)。

這個 NewSet 函數的返回值也可以作為其他 NewSet 函數的參數使用,比如上面的 SuperSet 作為參數使用。

injector

我們編寫程式把這些 providers 組合起來(比如下麵例子 initUserStore() 函數),wire 里的 wire 命令會按照依賴順序調用 providers 生成更加完整的函數,這個就是 injector。

首先,編寫生成 injector 的簽名函數,然後用 wire 命令生成相應的函數。

例子如下:

// +build wireinject

func initUserStore(info *ConnectionInfo) (*UserStore, error) {
    wire.Build(SuperSet, NewDB) // 聲明獲取 UserStore 需要調用哪些 provider 函數
    return nil, nil
}

然後用 wire 命令把上面的 initUserStore 函數生成 injector 函數,生成的函數對應文件名 wire_gen.go。

wire 命令:

You can generate the injector by invoking Wire in the package directory。

直接在生成 injector 函數的包下,使用 wire 命令,就可以生成 injector 代碼。

wire.Build() 函數:

它的參數可以是 wire.NewSet() 組織的一個或多個 provider,也可以直接使用 provider。

wire 使用

wire 結構體和方法列表

func Build(...interface{}) string
type Binding
	func Bind(iface, to interface{}) Binding
type ProvidedValue
	func InterfaceValue(typ interface{}, x interface{}) ProvidedValue
	func Value(interface{}) ProvidedValue
type ProviderSet
	func NewSet(...interface{}) ProviderSet
type StructFields
	func FieldsOf(structType interface{}, fieldNames ...string) StructFields
type StructProvider
	func Struct(structType interface{}, fieldNames ...string) StructProvider

更詳細說明可以看這裡 func index - pkg.go.dev

wire 安裝

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

快速開始

例子1

先新建一個 basics 的文件夾,然後在 basics 里使用 go mod init basics,新建一個 go.mod,在 go.mod 里引入 wire:require github.com/google/wire v0.5.0

整個文件夾目錄結構:

image-20220410222040384

  1. 定義 providers

在 basics 文件夾下新建 basics.go 文件,寫入如下代碼:

package main

import (
	"context"
	"errors"
)

type Student struct {
	ClassNo int
}

// NewStudent 就是一個 provider,返回一個 Student
func NewStudent() Student {
	return Student{ClassNo: 10}
}

type Class struct {
	ClassNo int
}

// NewClass 就是一個 provider,返回一個 Class
func NewClass(stu Student) Class {
	return Class{ClassNo: stu.ClassNo}
}

type School struct {
	ClassNo int
}

// NewSchool 是一個 provider,返回一個 School
// 與上面 provider 不同的是,它還返回了一個錯誤信息
func NewSchool(ctx context.Context, class Class) (School, error) {
	if class.ClassNo == 0 {
		return School{}, errors.New("cannot provider school when class is 0")
	}
	return School{ClassNo: class.ClassNo}, nil
}
  1. 定義 injector

新建文件 wire.go,代碼如下:

// +build wireinject

package main

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

var SuperSet = wire.NewSet(NewStudent, NewClass, NewSchool)

func initSchool() (School, error) {
	wire.Build(SuperSet)
	return School{}, nil
}

// +build wireinject

這一行代碼一定要在包最上面聲明,表明這是一個準備被編譯的 injector

  1. 用 wire 命令生成 injector 函數代碼

wire 命令生成 injector 代碼,在 basics 目錄下執行 wire 命令:

$ wire
wire: D:\work\mygo\go-practice2\di\wire\basics\wire.go:9:1: inject initSchool: no provider found for context.Context needed by basics.School in provider set "SuperSet" (D:\work\mygo\go-practice2\di\wire\basics\wire.go:7:16)

wire: basics: generate failed
wire: at least one generate failure

報錯了,看看顯示出的錯誤信息,最主要是這一行信息:

inject initSchool: no provider found for context.Context needed by basics.School in provider set "SuperSet"

來看一看 initSchool 函數,果然沒有給它提供 context.Context 。我們來修改函數,引入 context 包,然後給 initSchool 函數增加參數 context.Context

func initSchool(ctx context.Context) (School, error) {
	wire.Build(SuperSet)
	return School{}, nil
}

再來用命令 wire 編譯:

$ wire
wire: basics: wrote D:\work\mygo\go-practice2\di\wire\basics\wire_gen.go

生成的 injector 代碼,wire_gen.go 文件,

// Code generated by Wire. DO NOT EDIT.

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

package main

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

// Injectors from wire.go:

func initSchool(ctx context.Context) (School, error) {
	student := NewStudent()
	class := NewClass(student)
	school, err := NewSchool(ctx, class)
	if err != nil {
		return School{}, err
	}
	return school, nil
}

// wire.go:

var SuperSet = wire.NewSet(NewStudent, NewClass, NewSchool)

小結

wire 使用的步驟:

  1. 先編寫 provider。
  2. 再編寫 injector:把相關 provider 組織在一起,成為一個 ProviderSet。
  3. 最後用 wire 命令編譯:wire 會根據 provider 之間相關依賴生成代碼。

wire.NewSet 函數:

它可以把 provider 集合起來。作用1分類:可以把一組相關的 provider 寫在一起組成 ProviderSet。作用1延伸第2個作用,避免 provider 過多難於管理。

wite.Build 函數:

func Build(...interface{}) string

它的參數是 provider 不定長列表。 把所有相關的 provider 組織在一起然後生成 injector 函數代碼。它是生成 injector 函數的模板函數。

綁定介面

上面例子1綁定的是結構體和構造函數。如果有介面 interface 參與呢,那怎麼辦?比如下麵的代碼,

type Fooer interface {
    Hello()
}

type Foo struct{}

func (f Foo)Hello() {
    fmt.Println("hello")
}

func Bar struct{}

func NewBar() Bar {
    return Bar{}
}

有介面 Fooer,這個怎麼綁定呢?這時候就可以用 [wire.Bind](wire/wire.go at v0.5.0 · google/wire · GitHub) 函數,

var bind = wire.Bind(new(Fooer), new(Foo))
var set = wire.NewSet(bind, NewBar)

// or
var set = wire.NewSet(wire.Bind(new(Fooer), new(Foo)), NewBar)

struct prividers

struct 也可以直接當作一個 provider 使用。如果結構體的 provider 僅僅是用作欄位賦值,那麼可以使用函數 wire.Struct 來賦值。

type Foo int
type Bar int

func NewFoo() Foo {/* ... */}
func NewBar() Bar {/* ... */}

type FooBar struct {
    MyFoo Foo
    MyBar Bar
}

var set = wire.NewSet(
	NewFoo,
    NewBar,
    wire.Struct(new(FooBar), "MyFoo", "MyBar"),
)

更多信息請參考struct providers guide

Provider Set

上面例子1中就用到 provider set,把

相關的 provider 組織在一起。使用函數 wire.NewSet 就可以做到。

更多例子請查看官方文檔:

https://github.com/google/wire

參考

== just do it ==
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、約定編程 Spring AOP是一種約定流程的編程,咱們可以先通過動態代理模式的實現來理解Spring AOP的概念。 代理的邏輯很簡單,例如,當你需要採訪一名兒童時,首先需要經過他父母的同意,在一些問題上父母也許會替他回答,而對於另一些問題,也許父母覺得不太適合這個小孩會拒絕掉,顯然這時父母就 ...
  • 解釋器模式(Interpreter Design Pattern)指給定一個“語言”,定義它的文法的一種表示,並定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。這裡所指的“語言”是指使用規定格式和語法的代碼。 比如說在計算器中,我們輸入一個加法/減法表達式(中綴表達式)“1+6-5”字元串, ...
  • 《零基礎學Java》 線程的同步 在單線程程式中,每次只能做一件事情,後面的事情需要等待第一件事情完成後才可以進行。為此,Java提供了線程同步機制來防止多線程編程中搶占資源的問題。 線程安全 在編寫多線程程式時,應該考慮到線程安全問題。 模擬未考慮到線程安全問題的售票系統: public clas ...
  • 一、什麼是字典 字典是Python中最強大的數據類型之一,也是Python語言中唯一的映射類型。映射類型對象里哈希值(鍵,key)和指向的對象(值,value)是一對多的的關係,通常被認為是可變的哈希表,字典對象是可變的,它是一個容器類型,能存儲任意個數的Python對象,其中也可包括其他容器類型。 ...
  • package scanner;import java.util.Scanner;public class Demo4 { public static void main(String[] args){ Scanner s4=new Scanner(System.in); //從鍵盤接收數據 int ...
  • 今天是充實的一天 晨讀 你敢相信從早上6點40就起床了,跑去晨讀賺了0.1學分。 一早上的軟體測試 早八的正確打開方式就是進入了超星課堂,開啟了軟體測試的課堂,學習了等價類邊界值綜合(用戶登錄的測試),由於對新知識的熟悉度不好,整個早上做了四個版本,直到最後才完成,還錯過了提交時間,一整個要炸掉了。 ...
  • fastposter v2.7.1 緊急發佈 電商海報編輯器 fastposter海報生成器,電商海報編輯器,電商海報設計器,fast快速生成海報 海報製作 海報開發。二維碼海報,圖片海報,分享海報,二維碼推廣海報,支持Java Python PHP Go JS 小程式。基於Vue 和Pillow ...
  • 相信在座各位應該沒有幾個不看小說的吧,嘿嘿~ 一般來說咱們書荒的時候怎麼辦?自然是去起某點排行榜先找到小說名字,然後再找度娘一搜,哎 ,筆趣閣就出來答案了,美滋滋~ 但是那多麻煩,咱們直接用python,直接全部下載下來慢慢看不就好了~ 小孩子才做選擇,成年人選擇都要… 好了,不啰嗦了,等下大家要罵 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...