过去几年里我一直使用Java。最近,用Go建立了一个小项目,然而 Go 生态系统中依赖注入(DI)功能缺乏让我震惊。于是我决定尝试使用 Uber 的 dig 库来构建我的项目,期间感触颇深。
我发现 DI 帮助我解决了之前在 Go 应用程序中遇到的很多问题 - 过度使用 init 函数,滥用全局变量和复杂的应用程序设置等。
在这篇文章中,我将介绍 DI ,然后在使用 DI 框架(通过 dig 库)前后写一些例子做对比。
DI 的简要概述
依赖注入是指你的组件(通常在 go 中是 struct )在创建时,就应该获取它们依赖关系的一种思想。这与那些组件在初始化过程中,就建立自身依赖关系的反关联模式不同 。我们来看一个例子。
假设你构造Server 需要 Config 结构体。一种方法是在初始化期间 Server 构建 Config 。
type Server struct {
config *Config
}
func New() *Server {
return &Server{
config: buildMyConfigSomehow(),
}
}
看起来很方便。调用者甚至不必知道 Server 需要访问 Config 。这些都被我们的函数隐藏起来了。
然而,这存在一些缺点。首先,如果我们想要改变我们 Config 的构建方式,我们不得不改变所有调用构建代码的地方。例如,假设我们的 buildMyConfigSomehow 函数现在需要一个参数。每个调用处都需要访问该参数并需要将其传递给构造函数。
此外,这使得实现 Config 函数变得十分麻烦,我们得以某种方法进入 new 函数的内部,并创建Config 。
这是 DI 方式:
type Server struct {
config *Config
}
func New(config *Config) *Server {
return &Server{
config: config,
}
}
现在我们将 Server 与Config 分离 。我们可以根据自己的逻辑创造 Config 然后将结果传递给 New 函数。
此外,如果 Config 是一个接口,这为我们提供了一个简单的模拟途径 。只要 New 实现了我们的接口,就可以传递任何我们想要的东西。这使得测试实现了 Config 接口的 Server 很简单。
令人痛苦的是在创建 server 之前手动创建 config 。 我们在这里创建了一个依赖关系 – 因为 server 依赖 Config, 所以需要首先创建 Config 。在真正的应用程序中,这些依赖会变得更加复杂,这会导致构建应用程序完成其工作所需的组件间的复杂逻辑 。
这是 DI 框架可以提供帮助的地方。 DI 框架通常提供两个功能:
- “提供”新组件。简而言之,这告诉 DI 框架一旦你有这些组件,还需要其他什么组件(依赖关系)以及如何去构建。
- “检索”构建组件。
DI 框架通常基于您告诉它的“ providers ”构建依赖图并确定如何构建对象。这在没有具体例子的情况下很难理解,所以让我们来看一个中等大小的例子。
示例程序
我们来看http服务器端的代码 :客户端以 GET 方式请求 /people 路径时并返回 JSON 。我们将一步一步呈现代码,为简单起见,它们都存在于同一个包中(main)。请勿在真正的 Go 程序中执行此操作。可以在此处找到此示例的完整代码。
首先,让我们看看我们的 Person 。仅有一些被 JSON 标签标记的属性。
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
Person 有 Id,Name 和 Age 。
接下来让我们看看 Config 。与 Person 类似,它没有依赖关系。与 Person 不同的是,我们将提供构造函数。
type Config struct {
Enabled bool
DatabasePath string
Port string
}
func NewConfig() *Config {
return &Config{
Enabled: true,
DatabasePath: "./example.db",
Port: "8000",
}
}
Enabled 表示程序是否返回真实数据。DatabasePath 表示数据库的地址(使用 sqlite )。Port 表示服务器运行的端口。
下方函数用来打开数据库连接。它依赖于 Config 并返回 *sql.DB 。
接下来看看 PersonRepository。此结构负责从数据库中提取数据并反序列化为 Person 。
type PersonRepository struct {
database *sql.DB
}
func (repository *PersonRepository) FindAll() []*Person {
rows, _ := repository.database.Query(
`SELECT id, name, age FROM people;`
)
defer rows.Close()
people := []*Person{}
for rows.Next() {
var (
id int
name string
age int
)
rows.Scan(&id, &name, &age)
people = append(people, &Person{
Id: id,
Name: name,
Age: age,
})
}
return people
}
func NewPersonRepository(database *sql.DB) *PersonRepository {
return &PersonRepository{database: database}
}
PersonRepository的构建需要数据库连接。它有一个函数FindAll,此函数使用数据库连接信息并返回 Person 列表。
要在 HTTP 服务器和 PersonRepository 之间提供一层,我们需要创建 PersonService 。
type PersonService struct {
config *Config
repository *PersonRepository
}
func (service *PersonService) FindAll() []*Person {
if service.config.Enabled {
return service.repository.FindAll()
}
return []*Person{}
}
func NewPersonService(config *Config, repository *PersonRepository)
*PersonService {
return &PersonService{config: config, repository: repository}
}
我们的 PersonService 依赖于 Config 和 PersonRepository 。它有一个函数 FindAll ,如果启用了应用程序,则会有条件地调用 PersonRepository 。
最后,我们得到了 Server 。负责运行 HTTP 服务器并将适当的请求委托给 PersonService 。
type Server struct {
config *Config
personService *PersonService
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/people", s.people)
return mux
}
func (s *Server) Run() {
httpServer := &http.Server{
Addr: ":" + s.config.Port,
Handler: s.Handler(),
}
httpServer.ListenAndServe()
}
func (s *Server) people(w http.ResponseWriter, r *http.Request) {
people := s.personService.FindAll()
bytes, _ := json.Marshal(people)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(bytes)
}
func NewServer(config *Config, service *PersonService) *Server {
return &Server{
config: config,
personService: service,
}
}
Server 取决于 PersonService 和 Config 。
好的,我们了解了系统的所有组件。现在我们该如何在实际中初始化它们并启动我们的系统?