本节摘要:方法给类型挂行为,接收者的值形态与指针形态决定方法集;接口定义行为契约,实现是隐式的——不要声明"实现了某接口"。本节讲透接收者选择的工程准则、方法集规则、接口的组合威力、类型断言与类型开关,以及"接口的 nil 不等于 nil"这一经典陷阱的机理与解法。
阅读完本节,你应当能够:
type Rect struct{ W, H float64 } func (r Rect) Area() float64 { return r.W * r.H } // 值接收者 func (r *Rect) Scale(f float64) { r.W *= f; r.H *= f } // 指针接收者
接收者写在 func 与方法名之间,像"这个函数属于谁"。指针接收者能修改原件;值接收者拿到的是拷贝。Go 在调用时做自动取址:变量可寻址时 r.Scale(2) 自动变 (&r).Scale(2),所以日常调用无感。但感知在"放进接口"的时刻到来——这是方法集规则:
| 接收者形态 | 值变量 可调 | 指针变量 可调 | 值类型装入接口后 |
|---|---|---|---|
| 值接收者方法 | 是 | 是 | 有此方法 |
| 指针接收者方法 | 是(自动取址) | 是 | 没有此方法 |
推论:一个类型的方法里只要有一个指针接收者,就只应该用指针去满足接口,否则"编译期明明能调,装进接口却报缺方法"的错乱会找上门。
我的接收者选择准则:要么全是值,要么全是指针;结构体有修改需求或体积大 → 指针;小型不可变值(坐标、金额)→ 值;拿不准 → 指针。
type Shape interface { Area() float64 } func describe(s Shape) { fmt.Println(s.Area()) }
Rect 没有写过"implements Shape",只要方法签名对得上就自动满足。这带来一个与 Java 截然不同的能力:接口可以后置定义。调用方需要什么契约,自己声明;实现方甚至不需要知道接口存在。标准库的排序就是这个玩法——排序函数只要求类型提供三个方法(取长度、比较两元素、交换两元素),任何类型满足即排。
接口值内部是两字结构:动态类型指针加动态值指针。这个结构解释了那个著名陷阱:
var s Shape // nil接口:两字全空 var r *Rect = nil s = r // 动态类型是*Rect,动态值是nil fmt.Println(s == nil) // false!类型字非空,接口就不等于nil
⚠️ 常见坑:函数返回接口类型时,即使里面的指针是 nil,调用方判 nil 也会失败,随后解引用 panic。防御法:返回具体指针类型(nil 就是纯 nil),或返回接口前显式判空返回字面 nil。
type Reader interface { Read(p []byte) (int, error) } type Writer interface { Write(p []byte) (int, error) } type ReadWriter interface { Reader Writer }
标准库最核心的接口全部走"小接口组合"路线:一两个方法的 Reader、Writer 撑起了整个 IO 体系。接口越大,抽象越脆——"接口隔离"在 Go 不是建议而是生态习惯。设计顺序也应该反过来:先写具体类型,等第二个实现出现时再提炼接口,过早抽象是负债。

// 单目标断言,失败时ok为false if r, ok := s.(*Rect); ok { fmt.Println(r.W) } // 类型开关:接口版的多路分支 switch v := s.(type) { case *Rect: fmt.Println("矩形", v.W) case Circle: fmt.Println("圆", v.R) default: fmt.Println("未知形状") }
断言失败不加 ok 布尔值会直接 panic,生产代码一律用双返回值形态。类型开关是处理"异构集合"的标准姿势——错误处理里按错误类型分支(第 4 章)也靠它。
空接口 interface{} 没有任何方法,任何类型都满足它,泛型到来之前它就是"万能容器"。代价是丢失类型信息、处处断言、运行时才炸。Go 1.18 后有了类型参数,新代码应优先泛型:
func Max[T int | float64](a, b T) T { if a > b { return a } return b }
新版本还引入了预声明标识符 any 作为空接口的别名。空接口仍有领地:真正"任意值"的场景(日志参数、JSON 任意结构)。
💡 关键直觉:接口是"消费者定义的合同"。站在调用方写小接口,站在提供方写具体类型——两头都别越俎代庖,依赖自然反转。
问答补遗。接口能嵌接口吗? 能,就是组合(ReadWriter 的做法);接口不能嵌具体类型。一个类型能同时满足多个接口吗? 当然,方法集是超集即满足,这是隐式实现的红利。接口变量能比较吗? 动态类型可比较且值相等时 ==,否则 panic(如装了切片的接口比较直接炸),深度比较走反射。泛型接口化和接口泛型化怎么选? 类型在编译期已知用泛型(零运行时开销),运行时才知道或需要跨类型存储用接口——两者是互补不是替代。
下一章把类型装进包与模块,并直面错误处理。