![godi.svg](https://cdn.nlark.com/yuque/0/2020/svg/187192/1606043637443-734d0592-561c-4747-bf03-27bdc47d6837.svg#align=left&display=inline&height=150&margin=%5Bobject%20Object%5D&name=godi.svg&originHeight=150&originWidth=102&size=32823&status=done&style=none&width=102)
# 一、简介
当创建一个实例,并且该实例需要依赖时,就涉及到了依赖注入。
假设我们需要创建一个服务实例,该服务实例需要一个配置项实例。第一种实现思路是在初始化实例时自动创建,该操作是对创建者无感知的。
```go
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 的位置传入。
改造之后的代码是下面这样的:
```go
func NewServer(c *Config) *Server {
return &Server{c}
}
c := Config{"127.0.0.1", "8080"}
svc := NewServer(&c)
```
这样就把创建 Config 和创建 Server 的逻辑分离了。但是如果要创建 Server 实例,必须先创建一个 Config 实例。这就形成了依赖关系,依赖图如下:
[![Dfz4pD.png](https://s3.ax1x.com/2020/12/01/Dfz4pD.png)](https://imgchr.com/i/Dfz4pD)
在实际的应用中,依赖图可能更加复杂。
# 二、FindPerson 应用
下面来看一个更加复杂的案例。
假设在 mongodb 中有一个 SutraPavilion 数据库,其中有一个 Person 集合,数据格式如下:
![image.png](https://cdn.nlark.com/yuque/0/2020/png/187192/1606020155157-3410726e-ac0a-46ff-a06e-abcec2f664cd.png#align=left&display=inline&height=59&margin=%5Bobject%20Object%5D&name=image.png&originHeight=118&originWidth=542&size=42460&status=done&style=none&width=271)
接下来要从头构建一个 Web 应用将其中的数据以 JSON 的格式返回出去。
```bash
curl http://127.0.0.1:8080/person
```
```json
[{
"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 的配置项结构体和连接方法。
```go
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 的结构。
```go
package schema
type Person struct {
Id string `bson:"_id"`
Name string `json:"name"`
Age int `json:"age"`
}
```
在 services 中定义服务接口。
```go
package services
import (
"../schema"
)
type Person interface {
FindAll() []*schema.Person
}
```
在 impl 中实现接口。
```go
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 方法。
```go
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 结构体。
```go
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 函数中创建了大量的配置项实例。依赖图如下所示:
[![Dfx4Ln.md.png](https://s3.ax1x.com/2020/12/01/Dfx4Ln.md.png)](https://imgchr.com/i/Dfx4Ln)
随着应用的复杂度增加,不仅需要维护这些配置项及其依赖关系,而且还可能会有新加入的其他组件。
# 三、使用 DI 库 Dig 优化代码
dig 是 uber 开源的一款 DI 库。
dig 的实例称为容器(container),其中存储了所有的实例。
contaienr 提供了 Provide 和 Invoke 两个 API。
Provide 需要传入一个函数,该函数的返回值,也就是依赖的实例,会被存储到 container 中。同一种类型的实例仅会被存储一次。如果一个依赖在注入时需要依赖其他实例,container 会自动将所需依赖注入进去,只需要在函数的形式参数中声明即可。
Invoke 需要传入一个函数,该函数和 Provide 类似,可以自动从 container 中获取依赖实例。但是 Invoke 不会注入依赖。
```go
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](https://blog.drewolson.org/dependency-injection-in-go) software is fun
[Dependency injection in GO](https://golangforall.com/en/post/dependency-injection.html) golangforall
有疑问加站长微信联系(非本文作者))