函数、方法与接口
函数、方法与接口 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这课把书签存取从 main 函数里拆出来:定义 Store 接口,实现内存版,让业务代码只依赖接口。读完你能按「使用方需要什么」来设计接口,并完成一次实现替换。
问题场景
G02 的 CountByTag 直接吃切片,数据一进数据库就要重写所有调用点。更实际的问题:测试想用假数据、生产想用 SQLite,两套实现不该污染业务代码。Go 的做法是业务侧定义一个窄接口,两种实现各自满足它,切换只在装配的那一行发生。
核心解释
接口只写使用方需要的方法:
type BookmarkStore interface {
Add(b Bookmark) (int64, error)
Get(id int64) (Bookmark, error)
List() ([]Bookmark, error)
}
内存版实现,用指针接收者:
var ErrNotFound = errors.New("bookmark not found") // G04 讲透错误设计
type MemoryStore struct {
mu sync.Mutex
next int64
byID map[int64]Bookmark
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{byID: make(map[int64]Bookmark)}
}
func (s *MemoryStore) Add(b Bookmark) (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.next++
b.ID = s.next
s.byID[b.ID] = b
return b.ID, nil
}
func (s *MemoryStore) Get(id int64) (Bookmark, error) {
s.mu.Lock()
defer s.mu.Unlock()
b, ok := s.byID[id]
if !ok {
return Bookmark{}, ErrNotFound
}
return b, nil
}
Go 的接口是隐式满足的:MemoryStore 写对了方法签名,就自动是 BookmarkStore,不需要 implements 声明。这带来一条黄金法则:接口定义在使用方一侧——调用方只需要遍历,就只依赖 List,不要上来先设计十方法的「万能仓库接口」。标准库到处是单方法接口(io.Reader、fmt.Stringer),窄接口才有第二、第三种实现。
接收者的选择在这里很关键:Add 要改 s.next 和 s.byID,必须用指针接收者。写成值接收者的话,方法操作的是副本,改了也白改,而且编译不报错。惯例是:只要有一个方法用指针接收者,整个类型的所有方法统一用指针接收者。
替换实现时业务代码一行不改:
func CountByTag(s BookmarkStore, tag string) (int, error) {
bs, err := s.List()
if err != nil {
return 0, fmt.Errorf("count by tag %q: %w", tag, err)
}
n := 0
for _, b := range bs {
for _, t := range b.Tags {
if t == tag {
n++
}
}
}
return n, nil
}
CountByTag 的参数是接口类型,传 *MemoryStore 或将来的 *SQLiteStore 都能编译,G08 写好 SQLiteStore 后换装配行就完成迁移。
示例拆解
模块 bookmark 下组织成三个文件:
store.go // BookmarkStore 接口 + MemoryStore + ErrNotFound
main.go // 构造 store,Add 几条,调用 CountByTag 打印
go run . 验证:添加三个书签(两个带 go 标签),输出 count=2。验证接口确实生效有个惯用技巧:在 store.go 里写一行 var _ BookmarkStore = (*MemoryStore)(nil)。这是编译期接口断言,方法签名不匹配编译立刻报错,不用等运行才发现。另一个惯例藏在构造函数里——返回具体类型 *MemoryStore 而不是接口:参数收接口、返回给具体,给调用方留升级空间。
动手任务
- 给接口加 Delete(id int64) error,同步更新 MemoryStore:不存在时返回 ErrNotFound。
- 写第二个实现 FilteredStore:内嵌一个 BookmarkStore,List 时过滤掉不含指定标签的书签,体验不改原实现就扩展行为。
- 用 MemoryStore 和 FilteredStore 分别调用 CountByTag,确认同一份代码两种行为。
常见错误
- 用值接收者改字段:方法拿到的是副本,改了外部看不见,这种 bug 编译器不报。
- 先设计大接口再写实现:方法越堆越多,第二个实现写不动。接口从使用方需求里长出来。
- 忘记初始化 map 字段:直接 Add 会 panic(assignment to entry in nil map),构造函数里必须 make。
- 为了「通用」在接口里返回 interface{}:丢掉类型信息,调用方被迫做类型断言,错误推迟到运行时。
检查与复盘
完成下方检查题。自查清单:
- 能说出「接口定义在使用方一侧」的原因
- 新类型实现接口时习惯加
var _ 接口 = (*实现)(nil)断言 - 构造函数返回具体类型,函数参数收接口
参考资料
- Go Spec:接口类型 —— 隐式满足与类型断言的语法
- Effective Go:接口 —— 官方的接口设计建议
- 标准库 fmt.Stringer —— 标准库单方法接口的典型范例
读完这一课了?
标记完成后,工作台会安排接下来的学习。