Go 接口与组合
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
Go 没有 implements 关键字:只要方法签名匹配,类型就自动实现了接口。这条规则让接口可以后置定义,也带来了「接口为 nil 却不是 nil」这类陷阱。
声明与隐式实现
type Describer interface {
Describe() string
}
type User struct {
Name string
}
func (u User) Describe() string {
return "用户:" + u.Name
}
没有声明,User 已经实现了 Describer:
var d Describer = User{Name: "ann"}
fmt.Println(d.Describe()) // 用户:ann
编译期校验某个类型是否满足接口,用空标识符断言:
var _ Describer = (*User)(nil)
这行代码不影响运行,但类型不匹配时会编译失败,比运行时才发现好得多。
小接口原则
标准库的接口都很小:
type Stringer interface{ String() string }
type Reader interface{ Read(p []byte) (int, error) }
type Writer interface{ Write(p []byte) (int, error) }
接口越窄,实现者越多,复用性越强。需要多个能力时组合它们:
type ReadWriter interface {
Reader
Writer
}
定义接口的位置也有讲究:在使用方定义,而不是实现方。这样实现方不需要预知所有调用场景,也避免「为了接口而接口」。
// 消费方定义自己需要的最小能力
type UserStore interface {
Find(id int) (*User, error)
}
type Service struct {
store UserStore // 依赖接口,便于测试替身
}
any:空接口
any 是 interface{} 的别名,表示「任意类型」:
func printAny(v any) {
fmt.Println(v)
}
var x any = 42
x = "hello" // 可以随便换类型
any 放弃了类型检查,用于真正泛化的场景(日志、JSON、容器)。能用泛型或具体类型解决时不要用 any。
类型断言
var v any = "hello"
s, ok := v.(string) // 安全断言,失败时 ok 为 false,s 为零值
fmt.Println(s, ok) // hello true
n := v.(int) // 直接断言,失败会 panic
优先用 comma-ok 形式,只有在你确信类型时(比如刚插入 map 的值)才用直接断言。
类型 switch 处理多种可能类型:
switch x := v.(type) {
case string:
fmt.Println("字符串长度", len(x))
case int, int64:
fmt.Printf("整数 %v\n", x)
case error:
fmt.Println("错误", x.Error())
default:
fmt.Printf("其他 %T\n", x)
}
接口值为 nil 的陷阱
type MyErr struct{}
func (e *MyErr) Error() string { return "boom" }
func do() error {
var e *MyErr = nil
return e // 返回的不是 nil!
}
err := do()
fmt.Println(err == nil) // false
原因:接口由动态类型 + 动态值两部分组成。只要类型信息存在(*MyErr),接口本身就不为 nil,即使值是 nil 指针。
正确写法是返回真正的 nil:
func do() error {
return nil
}
或者调用方判断时先断言再检查指针。这类 bug 在「从函数里取到 nil 指针后直接返回」时反复出现。
组合:嵌入结构体与接口
Go 用组合替代继承。嵌入结构体复用字段与方法(见 Go Map 与结构体),嵌入接口则用于局部实现:
type Logger interface {
Info(msg string)
Error(msg string)
}
// 只实现 Info,其余方法由嵌入的 Logger 提供
type Service struct {
Logger
name string
}
func (s *Service) Info(msg string) {
s.Logger.Info("[" + s.name + "] " + msg)
}
外层方法遮蔽内层,不是「重写」,也不会发生动态派发到内层再回调外层——这正是 Go 与面向对象继承的关键差异。
一个完整例子:依赖注入与测试替身
package main
import (
"errors"
"fmt"
)
type Store interface {
Get(key string) (string, error)
}
type MemoryStore struct {
data map[string]string
}
func (m *MemoryStore) Get(key string) (string, error) {
v, ok := m.data[key]
if !ok {
return "", errors.New("key not found: " + key)
}
return v, nil
}
type Service struct {
store Store
}
func (s *Service) Lookup(key string) string {
v, err := s.store.Get(key)
if err != nil {
return "(缺失)"
}
return v
}
func main() {
store := &MemoryStore{data: map[string]string{"name": "ann"}}
svc := &Service{store: store}
fmt.Println(svc.Lookup("name"))
fmt.Println(svc.Lookup("city"))
}
// ann
// (缺失)
测试里把 Store 换成返回固定值的假实现,就能不碰真实存储地测 Service。
常见错误与注意事项
- 返回 nil 指针给接口:接口不为 nil,用
err != nil判断会误判。 - 用值类型实现指针方法后赋给接口:编译报错,改传
&T{}。 - 定义过大的接口:实现成本高、测试替身难写,拆成小接口。
- 用
any代替泛型或具体类型:丢失类型检查,处处要断言。 - 直接断言不做 ok 判断:类型不符直接 panic。
- 期望继承式的「重写回调」:嵌入只是提升与遮蔽,没有动态派发。
小结
- 接口隐式实现,无需
implements;用var _ I = (*T)(nil)做编译期校验。 - 接口要小,在使用方定义,按需组合。
- 断言优先
v, ok := x.(T),多类型分支用类型 switch。 - 接口的 nil 判断要看动态类型,nil 指针装进接口后不为 nil。
- 组合(嵌入结构体 / 接口)替代继承,方法遮蔽而非重写。
下一步
- 回到系列第一篇:Go 环境搭建与第一个程序
- 复习:Go 指针、方法与值语义
- 复习:Go Map 与结构体