Golang 依赖注入:入门篇

1,793 阅读4分钟

一、简介

当创建一个实例,并且该实例需要依赖时,就涉及到了依赖注入。 假设我们需要创建一个服务实例,该服务实例需要一个配置项实例。第一种实现思路是在初始化实例时自动创建,该操作是对创建者无感知的。

package main

import "net/http"

type Config struct {
	address string
	port    string
}

type Server struct {
	config *Config
}

func NewServer() *Server {
	return &Server{BuildConfig()}
}

func BuildConfig() *Config {
	return &Config{"127.0.0.1", "8080"}
}

func main() {
	svc := NewServer()
	http.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		resp.Write([]byte("di"))
	})
	http.ListenAndServe(svc.config.address+":"+svc.config.port, nil)
}

虽然非常方便的创建了 Config 对象,但是存在一个问题,就是不利于扩展,如果想要自己设置 Config 实例,就需要给 BuildConfig 函数传递参数,而该参数可能需要在所有 NewServer 的位置传入。 改造之后的代码是下面这样的。

func NewServer(c *Config) *Server {
	return &Server{c}
}

c := Config{"127.0.0.1", "8080"}
svc := NewServer(&c)

这样就把创建 Config 和创建 Server 的逻辑分离了。但是如果要创建 Server 实例,必须先创建一个 Config 实例。这就形成了依赖关系,依赖图如下。 依赖图1

在实际的应用中,依赖图可能更加复杂。

二、FindPerson 应用

下面来看一个更加复杂的案例。 假设在 mongodb 中有一个 SutraPavilion 数据库,其中有一个 Person 集合,数据格式如下:

数据

接下来要从头构建一个 Web 应用将其中的数据以 JSON 的格式返回出去。

curl http://127.0.0.1:8080/person
[{"Id":"5fb9e8c780efe11bf021fd35","name":"达摩祖师","age":65535},{"Id":"5fb9ec1880efe11bf021fd36","name":"张三丰","age":1024}]

文件的目录结构如下。

|____mgo           // mongodb 连接方法
| |____mgo.go
|____schema        // 定义数据结构
| |____Person.go
|____controllers   // 定义控制器
| |____person.go
|____main.go       // 主方法
|____services      // 定义业务员逻辑接口
| |____impl        // 实现业务逻辑
| | |____person.go
| |____person.go

在 mgo 中定义 mongodb 的配置项结构体和连接方法。

package mgo

import (
	"context"
	"fmt"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type Config struct {
	Host     string
	Port     string
	Username string
	Password string
	Database string
}

func ConnectDatabase(c *Config) (*mongo.Database, error) {
	uri := fmt.Sprintf("mongodb://%s:%s@%s:%s/%s?authSource=admin", c.Username, c.Password, c.Host, c.Port, c.Database)
	clientOptions := options.Client().ApplyURI(uri)
	client, err := mongo.Connect(context.TODO(), clientOptions)
	if err != nil {
		panic(err)
	}
	db := client.Database(c.Database)
	return db, err
}

在 schema 中定义 person 的结构。

package schema

type Person struct {
	Id   string `bson:"_id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

在 services 中定义服务接口。

package services

import (
	"../schema"
)

type Person interface {
	FindAll() []*schema.Person
}

在 impl 中实现接口。

package impl

import (
	"context"
	"log"

	"../../schema"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
)

type Person struct {
	Db *mongo.Database
}

func (p Person) FindAll() []*schema.Person {
	var result []*schema.Person
	cur, err := p.Db.Collection("person").Find(context.TODO(), bson.D{{}})
	if err != nil {
		log.Fatal(err)
	}
	for cur.Next(context.TODO()) {
		var elem schema.Person
		err := cur.Decode(&elem)
		if err != nil {
			log.Fatal(err)
		}
		result = append(result, &elem)
	}
	return result
}

在 controllers 中定义 FindAll 方法。

package controllers

import (
	"encoding/json"
	"net/http"

	"../services"
)

type Person struct {
	Service services.Person
}

func (p *Person) FindAll(w http.ResponseWriter, r *http.Request) {
	people := p.Service.FindAll()
	bytes, _ := json.Marshal(people)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(bytes)
}

最终在 main 函数中定义 Server 结构体。

package main

import (
	"net/http"

	"./controllers"
	"./mgo"
	"./services/impl"
)

type ServerConfig struct {
	Host string
	Port string
}
type Server struct {
	serverConfig *ServerConfig
	routes       *map[string]http.HandlerFunc
}

func (svc *Server) Run() {
	for path := range *svc.routes {
		http.HandleFunc(path, (*svc.routes)[path])
	}
	http.ListenAndServe(svc.serverConfig.Host+":"+svc.serverConfig.Port, nil)
}

func main() {
	// 创建 mongodb 配置
	mgoCfg := mgo.Config{
		Host:     "127.0.0.1",
		Port:     "27017",
		Username: "admin",
		Password: "UgOnvFDYyxZa0PR3jdp2",
		Database: "SutraPavilion",
	}
	Db, _ := mgo.ConnectDatabase(&mgoCfg)
	// 创建 person service
	personService := impl.Person{
		Db: Db,
	}
	// 创建 person controller
	personController := controllers.Person{
		Service: personService,
	}
	// 创建 routes 配置
	routes := make(map[string]http.HandlerFunc)
	routes["/person"] = personController.FindAll
	// 创建 server 配置
	svcCfg := ServerConfig{
		Host: "127.0.0.1",
		Port: "8080",
	}
	svc := Server{&svcCfg, &routes}
	// 启动服务
	svc.Run()
}

可以看到,在 main 函数中创建了大量的配置项实例。依赖图如下所示: 依赖图2

随着应用的复杂度增加,不仅需要维护这些配置项及其依赖关系,而且还可能会有新加入的其他组件。

三、使用 DI 库 Dig 优化代码

dig 是 uber 开源的一款 DI 库。 dig 的实例称为容器(container),其中存储了所有的实例。 contaienr 提供了 Provide 和 Invoke 两个 API。 Provide 需要传入一个函数,该函数的返回值,也就是依赖的实例,会被存储到 container 中。同一种类型的实例仅会被存储一次。如果一个依赖在注入时需要依赖其他实例,container 会自动将所需依赖注入进去,只需要在函数的形式参数中声明即可。 Invoke 需要传入一个函数,该函数和 Provide 类似,可以自动从 container 中获取依赖实例。但是 Invoke 不会注入依赖。

package main

import (
	"net/http"

	"./controllers"
	"./mgo"
	"./services/impl"
	"go.mongodb.org/mongo-driver/mongo"
	"go.uber.org/dig"
)

type ServerConfig struct {
	Host string
	Port string
}
type Router struct {
	routes *map[string]http.HandlerFunc
}
type Server struct {
	serverConfig *ServerConfig
	router       *Router
}

func (svc *Server) Run() {
	for path := range *svc.router.routes {
		http.HandleFunc(path, (*svc.router.routes)[path])
	}
	http.ListenAndServe(svc.serverConfig.Host+":"+svc.serverConfig.Port, nil)
}

func NewMogConfig() *mgo.Config {
	return &mgo.Config{
		Host:     "127.0.0.1",
		Port:     "27017",
		Username: "admin",
		Password: "UgOnvFDYyxZa0PR3jdp2",
		Database: "SutraPavilion",
	}
}
func NewDB(mgoCfg *mgo.Config) *mongo.Database {
	Db, _ := mgo.ConnectDatabase(mgoCfg)
	return Db
}
func NewPersonService(Db *mongo.Database) *impl.Person {
	return &impl.Person{
		Db: Db,
	}
}
func NewPersonController(personService *impl.Person) *controllers.Person {
	return &controllers.Person{
		Service: *personService,
	}
}
func NewRouter(personController *controllers.Person) *Router {
	// 创建 routes 配置
	routes := make(map[string]http.HandlerFunc)
	routes["/person"] = personController.FindAll
	return &Router{&routes}
}
func NewServerConfig() *ServerConfig {
	return &ServerConfig{
		Host: "127.0.0.1",
		Port: "8080",
	}
}
func NewServer(svcCfg *ServerConfig, router *Router) *Server {
	return &Server{svcCfg, router}
}

func BuildContainer() *dig.Container {
	container := dig.New()
	container.Provide(NewMogConfig)
	container.Provide(NewDB)
	container.Provide(NewPersonService)
	container.Provide(NewPersonController)
	container.Provide(NewRouter)
	container.Provide(NewServerConfig)
	container.Provide(NewServer)
	return container
}

func main() {
	container := BuildContainer()
	// 启动服务
	err := container.Invoke(func(server *Server) {
		server.Run()
	})
	if err != nil {
		panic(err)
	}
}

使用依赖注入,可以减少很多 init 函数和全局变量。提高代码可维护性。

四、参考链接

Dependency Injection in Go software is fun

Dependency injection in GO golangforall