Menu
CoddyTech

依赖注入

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 45 课,共 107 课。

依赖注入是一种让结构体从外部接收依赖项,而不是在内部创建依赖项的技术。在 Go 中,接口让这种模式变得自然而强大。

你不在结构体中硬编码特定实现,而是接收一个接口。这样,你就可以替换实现,而不必更改结构体的代码:

type Notifier interface {
    Send(message string) string
}

type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
    return "Email: " + message
}

type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
    return "SMS: " + message
}

现在创建一个依赖接口而不是具体类型的结构体:

type OrderService struct {
    notifier Notifier  // 通过接口注入的依赖
}

func NewOrderService(n Notifier) *OrderService {
    return &OrderService{notifier: n}
}

func (o *OrderService) PlaceOrder(item string) string {
    return o.notifier.Send("Order placed: " + item)
}

OrderService 不知道也不关心它使用的是电子邮件还是 SMS。创建服务时,你可以注入依赖项:

func main() {
    emailService := NewOrderService(EmailNotifier{})
    fmt.Println(emailService.PlaceOrder("Book"))  // Email:已下单:Book
    
    smsService := NewOrderService(SMSNotifier{})
    fmt.Println(smsService.PlaceOrder("Phone"))   // SMS:已下单:Phone
}

这种方式能让你的代码更灵活,也更容易测试。测试时,你可以注入一个不会实际发送消息的模拟通知器。在生产环境中,你可以注入真实的实现。两种情况下,结构体都保持不变。

challenge icon

挑战

简单

让我们构建一个日志系统,展示依赖注入的强大之处。你将创建一个服务,它可以将日志写入不同的目标位置,而服务本身并不知道,也不关心这些日志实际写到哪里。

你将把代码分成三个文件:

  • logger.go:定义一个 Logger 接口,该接口要求包含一个 Log(message string) string 方法。然后创建两种不同的日志记录器实现:
    • ConsoleLogger,包含一个 Prefix 字段。它的 Log 方法返回 [CONSOLE] [Prefix]: [message]
    • FileLogger,包含一个 Filename 字段。它的 Log 方法返回 [FILE:[Filename]] [message]
  • service.go:创建一个依赖于 Logger 接口(而不是具体类型)的 AppService 结构体。添加构造函数 NewAppService,它接收一个 Logger 并返回一个指向 AppService 的指针。为 AppService 添加一个名为 DoWork(task string) string 的方法,该方法使用注入的日志记录器记录消息 Processing: [task],并返回日志记录器返回的内容。
  • main.go:从输入中读取配置,创建两种日志记录器,将它们分别注入不同的 AppService 实例,并使用提供的任务调用每个服务的 DoWork。打印每次服务调用的结果。

输入如下:

  • 第 1 行:控制台日志记录器前缀
  • 第 2 行:文件日志记录器的文件名
  • 第 3 行:要处理的任务

例如,给定 INFO、app.log 和 user authentication,输出应为:

[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authentication

注意,AppService 并不知道日志是写入控制台还是文件。它只会对注入的日志记录器调用 Log。这种灵活性正是依赖注入的核心:同一份服务代码可以与完全不同的日志记录器实现配合使用。

亲手试试

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// 读取控制台日志记录器的前缀
	prefix, _ := reader.ReadString('\n')
	prefix = prefix[:len(prefix)-1]
	
	// 读取文件日志记录器的文件名
	filename, _ := reader.ReadString('\n')
	filename = filename[:len(filename)-1]
	
	// 读取要处理的任务
	task, _ := reader.ReadString('\n')
	if len(task) > 0 && task[len(task)-1] == '\n' {
		task = task[:len(task)-1]
	}
	
	// TODO:使用 prefix 创建 ConsoleLogger
	
	// TODO:使用 filename 创建 FileLogger
	
	// TODO:创建一个 AppService,并注入 ConsoleLogger
	
	// TODO:再创建一个 AppService,并注入 FileLogger
	
	// TODO:将 task 传给每个服务的 DoWork 方法,并打印结果
	fmt.Println("result1")
	fmt.Println("result2")
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器