Skip to content
All notes

包依赖与接口设计

讨论 Go 包循环依赖的处理方式,并用通知器示例说明接口与依赖注入。

包依赖与接口设计

循环依赖暴露了什么

a 导入 b,b 又导入 a,说明包的职责或依赖方向需要调整。处理方式取决于真正共享的内容:

  • 依赖行为:在使用方定义小接口,由外部传入实现。
  • 共享数据类型:把稳定、职责明确的类型放到独立包里。
  • 只需要一个操作:传入回调函数即可。

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

用计费服务理解依赖注入

计费服务完成支付后需要通知用户,但不必知道通知是邮件还是短信。依赖注入就是由外部创建通知器,再交给计费服务。

使用方定义接口

services/billing/billing.go 只依赖 Notify 这一行为:

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
}

通知器实现接口

Go 使用隐式接口实现;通知器只要具有相同的方法签名,就能传给计费服务,不需要导入 billing 包。

notifiers/email.go:

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:

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
}

这两个示例用打印代替实际发送,重点是依赖关系。

在入口组装

下面假设模块名为 project。切换通知方式只改变传入的实现,计费逻辑无需变化。

package main

import (
	"fmt"
	"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.Print("\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)
	}
}