前言
Go的接口是隐式实现的,不需要显式声明"实现了某个接口"。这种设计让代码解耦更自然,但也容易写出难以测试和维护的代码。
本文整理接口设计的常见模式和依赖注入的实践方法,目标是写出更易测试、更灵活的Go代码。
1. 接口设计原则
1.1 接口要小
Go社区有句话:接口越小越好。标准库里大量的接口只有一两个方法。
// io包里的经典小接口
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// 组合成更大的接口
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
为什么小接口更好:
- 实现成本低,更多类型可以满足
- 组合灵活,按需组合
- Mock简单,测试容易
1.2 在使用方定义接口
不要在实现方定义接口,而是在使用方按需定义。
// ❌ 不推荐:在实现方定义大接口
package user
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(u *User) error
UpdateUser(u *User) error
DeleteUser(id int) error
ListUsers(page, size int) ([]*User, error)
SearchUsers(query string) ([]*User, error)
// ... 更多方法
}
type userServiceImpl struct {
db *sql.DB
}
func NewUserService(db *sql.DB) UserService {
return &userServiceImpl{db: db}
}
// ✅ 推荐:在使用方按需定义小接口
package order
// 只定义需要的方法
type UserGetter interface {
GetUser(id int) (*user.User, error)
}
type OrderService struct {
users UserGetter // 依赖接口,不是具体实现
}
func (s *OrderService) CreateOrder(userID int, items []Item) error {
u, err := s.users.GetUser(userID)
if err != nil {
return err
}
// 创建订单逻辑
}
1.3 接受接口,返回结构体
// ✅ 推荐
func NewServer(logger Logger, db Database) *Server {
return &Server{
logger: logger,
db: db,
}
}
// ❌ 不推荐
func NewServer(logger Logger, db Database) ServerInterface {
return &Server{
logger: logger,
db: db,
}
}
原因:
- 返回具体类型,调用方可以访问所有方法
- 返回接口会限制调用方,失去灵活性
- 接口应该在使用方定义,不是实现方
2. 常见接口设计模式
2.1 策略模式
不同的算法封装成接口,运行时切换。
// 定义策略接口
type Compressor interface {
Compress(data []byte) ([]byte, error)
Decompress(data []byte) ([]byte, error)
}
// Gzip实现
type GzipCompressor struct{}
func (g *GzipCompressor) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (g *GzipCompressor) Decompress(data []byte) ([]byte, error) {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer r.Close()
return io.ReadAll(r)
}
// Zstd实现
type ZstdCompressor struct{}
func (z *ZstdCompressor) Compress(data []byte) ([]byte, error) {
// zstd压缩实现
}
func (z *ZstdCompressor) Decompress(data []byte) ([]byte, error) {
// zstd解压实现
}
// 使用方
type FileStorage struct {
compressor Compressor
}
func (s *FileStorage) Save(filename string, data []byte) error {
compressed, err := s.compressor.Compress(data)
if err != nil {
return err
}
return os.WriteFile(filename, compressed, 0644)
}
2.2 装饰器模式
在不修改原有实现的情况下增加功能。
// 基础接口
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// 标准库的http.Client满足这个接口
var _ HTTPClient = (*http.Client)(nil)
// 带日志的装饰器
type LoggingClient struct {
client HTTPClient
logger Logger
}
func (c *LoggingClient) Do(req *http.Request) (*http.Response, error) {
start := time.Now()
c.logger.Info("request", "method", req.Method, "url", req.URL.String())
resp, err := c.client.Do(req)
c.logger.Info("response",
"method", req.Method,
"url", req.URL.String(),
"duration", time.Since(start),
"status", resp.StatusCode,
)
return resp, err
}
// 带重试的装饰器
type RetryClient struct {
client HTTPClient
maxRetries int
backoff time.Duration
}
func (c *RetryClient) Do(req *http.Request) (*http.Response, error) {
var lastErr error
for i := 0; i <= c.maxRetries; i++ {
resp, err := c.client.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
lastErr = err
time.Sleep(c.backoff * time.Duration(i+1))
}
return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}
// 组合使用
func NewHTTPClient() HTTPClient {
base := &http.Client{Timeout: 10 * time.Second}
withRetry := &RetryClient{client: base, maxRetries: 3, backoff: time.Second}
withLogging := &LoggingClient{client: withRetry, logger: defaultLogger}
return withLogging
}
2.3 适配器模式
将不兼容的接口转换为期望的接口。
// 目标接口
type Cache interface {
Get(key string) ([]byte, error)
Set(key string, value []byte, ttl time.Duration) error
Delete(key string) error
}
// Redis客户端(第三方库的接口)
type RedisClient struct {
// ...
}
func (r *RedisClient) GetString(key string) (string, error) { ... }
func (r *RedisClient) SetEx(key string, value string, seconds int) error { ... }
func (r *RedisClient) Del(keys ...string) error { ... }
// 适配器
type RedisAdapter struct {
client *RedisClient
}
func (a *RedisAdapter) Get(key string) ([]byte, error) {
s, err := a.client.GetString(key)
if err != nil {
return nil, err
}
return []byte(s), nil
}
func (a *RedisAdapter) Set(key string, value []byte, ttl time.Duration) error {
return a.client.SetEx(key, string(value), int(ttl.Seconds()))
}
func (a *RedisAdapter) Delete(key string) error {
return a.client.Del(key)
}
// 使用
var _ Cache = (*RedisAdapter)(nil)
3. 依赖注入
3.1 构造函数注入
最常用的方式,依赖通过构造函数传入。
// 定义依赖接口
type Logger interface {
Info(msg string, args ...interface{})
Error(msg string, args ...interface{})
}
type UserRepository interface {
GetByID(id int) (*User, error)
Save(u *User) error
}
type EmailSender interface {
Send(to, subject, body string) error
}
// 服务定义
type UserService struct {
logger Logger
repo UserRepository
email EmailSender
}
// 构造函数注入
func NewUserService(logger Logger, repo UserRepository, email EmailSender) *UserService {
return &UserService{
logger: logger,
repo: repo,
email: email,
}
}
func (s *UserService) Register(email, password string) (*User, error) {
s.logger.Info("registering user", "email", email)
user := &User{
Email: email,
Password: hashPassword(password),
}
if err := s.repo.Save(user); err != nil {
s.logger.Error("failed to save user", "error", err)
return nil, err
}
if err := s.email.Send(email, "Welcome", "Welcome to our service!"); err != nil {
s.logger.Error("failed to send welcome email", "error", err)
// 不返回错误,发邮件失败不影响注册
}
return user, nil
}
3.2 函数选项模式
适合有多个可选依赖的情况。
type Server struct {
addr string
logger Logger
db Database
cache Cache
timeout time.Duration
}
type Option func(*Server)
func WithLogger(l Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func WithDatabase(db Database) Option {
return func(s *Server) {
s.db = db
}
}
func WithCache(c Cache) Option {
return func(s *Server) {
s.cache = c
}
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
logger: defaultLogger, // 默认值
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// 使用
server := NewServer(":8080",
WithLogger(customLogger),
WithDatabase(mysqlDB),
WithTimeout(60*time.Second),
)
3.3 手动组装依赖
对于小型项目,在main函数里手动组装足够。
func main() {
// 基础设施层
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
db, err := sql.Open("mysql", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer db.Close()
redisClient := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_URL"),
})
// 仓储层
userRepo := repository.NewUserRepository(db)
orderRepo := repository.NewOrderRepository(db)
// 服务层
emailSender := email.NewSMTPSender(os.Getenv("SMTP_HOST"))
cache := cache.NewRedisCache(redisClient)
userService := service.NewUserService(logger, userRepo, emailSender)
orderService := service.NewOrderService(logger, orderRepo, userRepo, cache)
// HTTP层
userHandler := handler.NewUserHandler(userService)
orderHandler := handler.NewOrderHandler(orderService)
// 路由
mux := http.NewServeMux()
mux.HandleFunc("/users", userHandler.Handle)
mux.HandleFunc("/orders", orderHandler.Handle)
// 启动
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
log.Fatal(server.ListenAndServe())
}
3.4 使用Wire做依赖注入
项目大了之后,手动组装依赖很繁琐。Wire是Google出的依赖注入代码生成工具。
// wire.go
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
)
func InitializeApp() (*App, error) {
wire.Build(
// 提供者
NewConfig,
NewLogger,
NewDatabase,
NewRedisClient,
// 仓储
repository.NewUserRepository,
repository.NewOrderRepository,
// 服务
service.NewUserService,
service.NewOrderService,
// Handler
handler.NewUserHandler,
handler.NewOrderHandler,
// App
NewApp,
)
return nil, nil
}
运行wire命令,自动生成组装代码。
4. 测试中的接口使用
4.1 手写Mock
简单场景下,手写Mock最直接。
// 生产代码
type UserRepository interface {
GetByID(id int) (*User, error)
Save(u *User) error
}
// 测试用Mock
type MockUserRepository struct {
GetByIDFunc func(id int) (*User, error)
SaveFunc func(u *User) error
}
func (m *MockUserRepository) GetByID(id int) (*User, error) {
if m.GetByIDFunc != nil {
return m.GetByIDFunc(id)
}
return nil, nil
}
func (m *MockUserRepository) Save(u *User) error {
if m.SaveFunc != nil {
return m.SaveFunc(u)
}
return nil
}
// 测试
func TestUserService_Register(t *testing.T) {
mockRepo := &MockUserRepository{
SaveFunc: func(u *User) error {
if u.Email == "" {
return errors.New("email required")
}
u.ID = 1
return nil
},
}
mockEmail := &MockEmailSender{
SendFunc: func(to, subject, body string) error {
return nil
},
}
svc := NewUserService(testLogger, mockRepo, mockEmail)
user, err := svc.Register("test@example.com", "password")
assert.NoError(t, err)
assert.Equal(t, 1, user.ID)
}
4.2 使用mockgen
项目大了用mockgen自动生成Mock代码。
# 安装
go install go.uber.org/mock/mockgen@latest
# 生成
mockgen -source=repository.go -destination=mock_repository.go -package=repository
// 使用生成的Mock
func TestUserService_Register(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := NewMockUserRepository(ctrl)
mockEmail := NewMockEmailSender(ctrl)
// 设置期望
mockRepo.EXPECT().
Save(gomock.Any()).
DoAndReturn(func(u *User) error {
u.ID = 1
return nil
})
mockEmail.EXPECT().
Send("test@example.com", gomock.Any(), gomock.Any()).
Return(nil)
svc := NewUserService(testLogger, mockRepo, mockEmail)
user, err := svc.Register("test@example.com", "password")
assert.NoError(t, err)
assert.Equal(t, 1, user.ID)
}
4.3 表驱动测试
配合接口,表驱动测试很方便。
func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
userID int
setup func(*MockUserRepository)
want *User
wantErr bool
}{
{
name: "user exists",
userID: 1,
setup: func(m *MockUserRepository) {
m.GetByIDFunc = func(id int) (*User, error) {
return &User{ID: 1, Email: "test@example.com"}, nil
}
},
want: &User{ID: 1, Email: "test@example.com"},
},
{
name: "user not found",
userID: 999,
setup: func(m *MockUserRepository) {
m.GetByIDFunc = func(id int) (*User, error) {
return nil, ErrNotFound
}
},
wantErr: true,
},
{
name: "database error",
userID: 1,
setup: func(m *MockUserRepository) {
m.GetByIDFunc = func(id int) (*User, error) {
return nil, errors.New("connection refused")
}
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := &MockUserRepository{}
tt.setup(mockRepo)
svc := NewUserService(testLogger, mockRepo, nil)
got, err := svc.GetUser(tt.userID)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
5. 实际项目结构
一个典型的分层结构:
myapp/
├── cmd/
│ └── server/
│ └── main.go # 组装依赖,启动服务
├── internal/
│ ├── domain/ # 领域模型
│ │ ├── user.go
│ │ └── order.go
│ ├── repository/ # 数据访问层
│ │ ├── interface.go # 接口定义
│ │ ├── user.go # 实现
│ │ └── user_test.go
│ ├── service/ # 业务逻辑层
│ │ ├── user.go
│ │ └── user_test.go
│ └── handler/ # HTTP处理层
│ ├── user.go
│ └── user_test.go
├── pkg/ # 可复用的库
│ ├── logger/
│ └── cache/
└── go.mod
接口定义位置的选择:
// internal/repository/interface.go
// 仓储接口定义在repository包,因为实现也在这里
type UserRepository interface {
GetByID(id int) (*domain.User, error)
Save(u *domain.User) error
// ...
}
// internal/service/user.go
// 服务层依赖接口,但接口可能定义在其他包
type UserService struct {
repo repository.UserRepository // 或者在service包里定义小接口
logger Logger
}
总结
| 要点 | 说明 |
|---|---|
| 接口要小 | 一两个方法最好,便于实现和Mock |
| 在使用方定义接口 | 按需定义,避免大而全的接口 |
| 接受接口返回结构体 | 构造函数接收接口类型,返回具体类型 |
| 构造函数注入 | 最常用的依赖注入方式 |
| 函数选项模式 | 适合可选依赖多的情况 |
| Mock测试 | 小项目手写Mock,大项目用mockgen |
| 分层结构 | domain/repository/service/handler |
核心思想:
- 面向接口编程,但不要过度设计。不是所有东西都要抽接口,有明确需求再抽。
- 依赖倒置。高层模块不依赖低层模块,都依赖抽象。
- 可测试性。写代码时想着怎么测试,自然会写出松耦合的代码。
接口设计没有银弹,关键是理解原则背后的原因,然后根据项目实际情况做取舍。