依赖注入:入门篇

luzhenqian · · 160 次点击 · · 开始浏览    
![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

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:1006366459

160 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传