接口(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 时,只需要写一个包含 GetByID 和 Update 的 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,只需要写一个包含 GetByID 和 Update 的 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 所有包并进行组装。