接口(interface)是 Go 语言的核心,是实现系统模块解耦与扩展性的核心机制。

Go 语言接口采用隐式实现机制以实现接口定义与具体类型的完全解耦。与 Java 等需要显式声明继承关系的语言不同,Go 开发者无需显式声明类型继承关系,只需实现接口定义的全部方法签名即可满足类型契约。

1
2
// Java 需要显式实现接口
class Student implements Person {}

接口的形状由代码的使用方决定。代码的提供者和使用者不需要互相依赖。接入外部功能时,我们可以在本地写一个只包含必要功能的小型接口,直接接收外部对象。不用修改原有代码、不用提前做复杂规划。

 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
// 外部代码库提供了包含众多功能的复杂结构体
package thirdparty

type Client struct{}

func (c *Client) Fetch() []byte { return nil }
func (c *Client) Upload(data []byte) error { return nil }

// 本地只需上传功能即可自行定义小型接口
package myapp

type Uploader interface {
    Upload(data []byte) error
}

// 业务代码仅依赖本地定义的小型接口
func ProcessAndSync(u Uploader, data []byte) error {
    return u.Upload(data)
}

// 启动时直接传入外部结构体
func Init() {
    client := &thirdparty.Client{}
    ProcessAndSync(client, nil)
}

让使用者自己控制所需功能具有以下实际好处:

  • 减少多余代码,提升整体可读性。
  • 降低外部依赖,方便后期替换实现。

这种由调用方定义接口的模式,自然而然地引导我们走向小型接口的设计。接口包含的方法越少,它能适配的场景就越多。

以标准库中的io.Reader接口为例:

1
2
3
type Reader interface {
    Read(p []byte) (n int, err error)
}

它只定义了一个读取行为。正因为它的要求极低,无论文件、网络连接还是内存缓冲区,都能轻松满足这个要求。这体现了接口设计中的最小化原则。

示例

数据持久层(store)

不定义任何接口,直接写具体结构体,出参返回具体类型。

 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
42
// pkg/store/user_store.go
package store

import (
	"context"
	"gorm.io/gorm"
)

// GORM 模型定义
type User struct {
	ID   string `gorm:"primaryKey"`
	Name string
	Age  int
}

// 1. 直接写具体结构体,不定义任何 UserStore 接口
type UserStore struct {
	db *gorm.DB
}

// 2. 返回具体类型指针 *UserStore
func NewUserStore(db *gorm.DB) *UserStore {
	return &UserStore{db: db}
}

// 3. 将 GORM 的链式调用封装在语义化方法内部
func (s *UserStore) Create(ctx context.Context, u *User) error {
	return s.db.WithContext(ctx).Create(u).Error
}

func (s *UserStore) GetByID(ctx context.Context, id string) (*User, error) {
	var user User
	err := s.db.WithContext(ctx).First(&user, "id = ?", id).Error
	if err != nil {
		return nil, err
	}
	return &user, nil
}

func (s *UserStore) UpdateName(ctx context.Context, id string, name string) error {
	return s.db.WithContext(ctx).Model(&User{}).Where("id = ?", id).Update("name", name).Error
}

业务逻辑层(biz 或 service)

接口定义在使用它的地方。业务层需要什么,就声明什么小接口。

假设我们有一个用户密码重置/修改的业务,它只需要查询用户和更新用户,根本不需要删除用户的能力。

 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
// pkg/biz/user_service.go
package biz

import "pkg/store"

// 1. 消费方定义接口:只暴露该业务需要的 2 个方法(小接口)
type UserUpdater interface {
    GetByID(id string) (*store.User, error)
    Update(u *store.User) error
}

type UserService struct {
    // 2. 依赖自己定义的接口,而不是依赖 store.UserStore 结构体
    updater UserUpdater
}

func NewUserService(u UserUpdater) *UserService {
    return &UserService{updater: u}
}

// 3. 编写业务逻辑
func (s *UserService) UpdateUserName(id string, newName string) error {
    user, err := s.updater.GetByID(id)
    if err != nil {
        return err
    }
    user.Name = newName
    return s.updater.Update(user)
}

测试 UserService 时,只需要写一个包含 GetByIDUpdate 的 Mock 对象即可,零数据库依赖。

HTTP/API 层

处理 HTTP 请求,直接持有具体 Service 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// pkg/handler/user_handler.go
package handler

import (
    "net/http"
    "pkg/biz"
)

type UserHandler struct {
    svc *biz.UserService // 直接持有具体的 Service 指针
}

func NewUserHandler(svc *biz.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

func (h *UserHandler) HandleUpdateName(w http.ResponseWriter, r *http.Request) {
    // 解析请求参数...
    // 调用 h.svc.UpdateUserName(...)
}

入口依赖组装(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
// cmd/server/main.go
package main

import (
    "database/sql"
    "pkg/biz"
    "pkg/handler"
    "pkg/store"
)

func main() {
    db := initDB() // *sql.DB

    // 1. 创建具体的数据访问结构体 (*store.UserStore)
    userStore := store.NewUserStore(db)

    // 2. 传入 UserService,隐式转换为 biz.UserUpdater 接口
    userBiz := biz.NewUserService(userStore)

    // 3. 组装 Handler
    userHandler := handler.NewUserHandler(userBiz)

    // 4. 启动路由监听...
}

总结

假设 store.UserStore 随着业务演进,慢慢写了 20 个 CRUD 方法。biz 层的 UserService 无需任何修改,阅读 biz 代码的人一眼就能看懂这个业务只需要读取和更新用户,绝不会误调用删除或锁住用户。

其次,可以很方便地测试 UserService,只需要写一个包含 GetByIDUpdate 的 Mock 对象即可,不要处理不需要的方法,零数据库依赖。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// pkg/biz/user_service_test.go

// 只需要 5 行代码写一个假实现
type MockStore struct{ user *store.User }
func (m *MockStore) GetByID(id string) (*store.User, error) { return m.user, nil }
func (m *MockStore) Update(u *store.User) error            { m.user = u; return nil }

func TestUpdateUserName(t *testing.T) {
    mock := &MockStore{user: &store.User{ID: "1", Name: "Old"}}
    svc := NewUserService(mock) // 轻松注入

    err := svc.UpdateUserName("1", "New")
    // 断言测试...
}

不同模块之间的依赖关系变为单向流转,可避免循环调用:

  • store 包:只依赖基础库(如 gorm),完全不依赖 biz 或 handler。
  • biz 包:只依赖数据模型 store.User,不依赖 store 里的逻辑实现。
  • main.go:作为装配根(Composition Root),统一 import 所有包并进行组装。