Go 接口与组合

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

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。

常见错误与注意事项

  1. 返回 nil 指针给接口:接口不为 nil,用 err != nil 判断会误判。
  2. 用值类型实现指针方法后赋给接口:编译报错,改传 &T{}。
  3. 定义过大的接口:实现成本高、测试替身难写,拆成小接口。
  4. 用 any 代替泛型或具体类型:丢失类型检查,处处要断言。
  5. 直接断言不做 ok 判断:类型不符直接 panic。
  6. 期望继承式的「重写回调」:嵌入只是提升与遮蔽,没有动态派发。

小结

  • 接口隐式实现,无需 implements;用 var _ I = (*T)(nil) 做编译期校验。
  • 接口要小,在使用方定义,按需组合。
  • 断言优先 v, ok := x.(T),多类型分支用类型 switch。
  • 接口的 nil 判断要看动态类型,nil 指针装进接口后不为 nil。
  • 组合(嵌入结构体 / 接口)替代继承,方法遮蔽而非重写。

下一步