Go 的一些理解

如何解决循环依赖(Circular Dependency)

什么是循环依赖

  • package a 导入了 package b
  • package b 又反过来导入了 package a
1
2
3
4
5
6
  a.go           b.go
+--------+     +--------+
| package| --> | package|
|   a    |     |   b    |
|        | <-- |        |
+--------+     +--------+

依赖循环是设计的问题,如果遇到依赖的情况,需要重新思考该如何对项目进行设计。

解决循环依赖

核心思想是 打破循环。Go 社区推崇的最佳实践是利用 接口(Interface)依赖倒置原则(Dependency Inversion Principle, DIP)

方案一:使用接口(最推荐、最优雅的 “Go Way”)

依赖倒置原则的核心是 “依赖于抽象,而不是依赖于具体实现”。在 Go 中,这个“抽象”就是接口。

方案二:提取公共依赖到新包

如果循环仅仅是因为共享了某个数据结构(struct),那么最简单的办法就是把这个公共的数据结构提取到一个新的、更底层的包里。

警告:不要滥用这个模式,避免创建一个什么都往里扔的“垃圾桶”(common, utils)包。这个新包应该只包含稳定、底层、被广泛依赖的数据结构或常量。

Pixiu 里不是什么东西都能放进 modelcommon 里的,这点需要注意,希望最后不用重构。

方案三:使用回调函数

对于一些简单的依赖,使用回调函数是接口的一个轻量级替代方案。

场景user 包中的一个函数需要 order 包的某个功能,但整体上 user 包并不想依赖 order 包。

事实上,回调函数即是依赖注入的函数。

依赖注入(Dependency Injection, DI)解析

依赖注入的核心思想就是:一个组件(对象/结构体)不应该自己创建它所需要的依赖(其他组件),而应该由外部的、更高层次的组件来提供(“注入”)给它。

这种控制关系的反转(组件从主动创建依赖,变为被动接收依赖),也称为“控制反转”(Inversion of Control, IoC)。DI 是实现 IoC 的一种最常见的技术。

Go interface DI 中的标准

接口定义了行为契约:它只规定一个组件应该能做什么(有哪些方法),但不关心具体是怎么做的

隐式实现:这是 Go 接口的精髓。任何类型,只要它实现了接口中定义的所有方法,就被认为自动满足了这个接口,无需使用 implements 这样的关键字。

sample

一个计费服务 (BillingService),当用户支付账单后,需要发送一个通知

我们希望这个“通知”方式是可替换的,今天用邮件,明天可能想换成短信,测试的时候可能只想打印到控制台。

第 1 步:在“消费者”中定义接口(定义插槽标准)

BillingService 是依赖的消费者,因为它需要一个“通知器”的功能。所以,我们应该在 billing 包中定义这个接口。

原则:接口应该由消费者来定义(Define interfaces where they are used)。

services/billing/billing.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package billing

import "fmt"

// 1. 定义一个“通知器”接口,这是我们的“插槽标准”
// BillingService 不关心具体怎么发通知,它只需要一个能 Notify 的东西。
type Notifier interface {
	Notify(userID int, message string) error
}

// 2. BillingService 结构体,它包含一个接口类型的字段
type Service struct {
	notifier Notifier // 依赖的是抽象的接口,而不是具体的实现
}

// 3. 构造函数,接收一个满足 Notifier 接口的实例,并“注入”进来
func NewService(n Notifier) *Service {
	return &Service{
		notifier: n,
	}
}

// PayInvoice 是核心业务逻辑
func (s *Service) PayInvoice(userID int, amount float64) error {
	// ... 一些计费逻辑 ...
	fmt.Printf("Processing invoice for user %d, amount %.2f\n", userID, amount)

	// 4. 使用依赖(调用接口方法),它不知道具体是哪个实现在工作
	message := fmt.Sprintf("Your invoice for $%.2f has been paid.", amount)
	err := s.notifier.Notify(userID, message)
	if err != nil {
		return fmt.Errorf("failed to send notification: %w", err)
	}

	fmt.Println("Billing process completed successfully.")
	return nil
}
第 2 步:创建具体的实现(制造能插进插槽的零件)

现在,我们来创建几个不同的“通知器”,它们都符合 Notifier 接口标准。

notifiers/email.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package notifiers

import "fmt"

// EmailNotifier 是一个具体的实现
type EmailNotifier struct {
	// 可以有自己的字段,比如 SMTP 服务器地址等
	AdminEmail string
}

// 实现 Notifier 接口的 Notify 方法
func (e EmailNotifier) Notify(userID int, message string) error {
	// 实际的邮件发送逻辑
	fmt.Printf("--- Sending EMAIL to user %d ---\n", userID)
	fmt.Printf("Message: %s\n", message)
	fmt.Printf("Admin copy sent to: %s\n", e.AdminEmail)
	fmt.Println("------------------------------")
	return nil
}

notifiers/sms.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package notifiers

import "fmt"

// SMSNotifier 是另一个具体的实现
type SMSNotifier struct {
	APIToken string // 短信服务商的 token
}

// 同样实现 Notifier 接口的 Notify 方法
func (s SMSNotifier) Notify(userID int, message string) error {
	// 实际的短信发送逻辑
	fmt.Printf("--- Sending SMS to user %d ---\n", userID)
	fmt.Printf("Message: %s (Token: %s)\n", message, s.APIToken)
	fmt.Println("----------------------------")
	return nil
}

注意:EmailNotifierSMSNotifier 都不需要知道 billing 包的存在。它们只是默默地实现了自己的 Notify 方法。

第 3 步:在 main.go 中进行组装和注入(把零件插到主板上)

main.go 是我们程序的最高层。它负责创建具体的依赖实例,并将其注入到消费者中。

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
	"project/services/billing"
	"project/notifiers"
	"log"
)

func main() {
	// === 场景一:使用邮件通知 ===
	fmt.Println("### Running with Email Notifier ###")
	
	// 1. 创建一个具体的依赖实例 (邮件通知器)
	emailNotifier := notifiers.EmailNotifier{AdminEmail: "[email protected]"}

	// 2. 将依赖实例注入到 BillingService 的构造函数中
	//    因为 EmailNotifier 实现了 Notify(...) 方法,所以它满足 billing.Notifier 接口,可以被传入
	billingSvc1 := billing.NewService(emailNotifier)

	// 3. 调用业务方法
	if err := billingSvc1.PayInvoice(101, 99.95); err != nil {
		log.Fatal(err)
	}

	fmt.Println("\n=====================================\n")

	// === 场景二:切换到短信通知 ===
	fmt.Println("### Running with SMS Notifier ###")

	// 1. 创建另一个具体的依赖实例 (短信通知器)
	smsNotifier := notifiers.SMSNotifier{APIToken: "abcdef123456"}

	// 2. 将这个新的依赖实例注入
	//    注意:我们只是改变了传入的零件,billing.NewService 和 billingSvc 本身的代码完全不用动!
	billingSvc2 := billing.NewService(smsNotifier)

	// 3. 再次调用业务方法,行为已经改变
	if err := billingSvc2.PayInvoice(202, 49.50); err != nil {
		log.Fatal(err)
	}
}