Go 入门(07):接口 interface——隐式实现,鸭子类型
更新时间:2026-09-01。本文是
languages/go/beginner/入门层第 7 篇,接 方法。接口是 Go 多态的核心,设计非常简洁:你实现了接口的所有方法,就满足这个接口——不用写implements,隐式满足。
本文要回答的问题
- Go 接口和 Java/C++ 接口有什么不一样?什么是隐式实现?
- 什么是鸭子类型?为什么 Go 不用写 implements?
- nil 接口和 nil 指针有什么区别?这个坑为什么这么经典?
- 空接口
interface{}是什么?什么时候用?
一、基本定义
Go 接口定义一组方法签名:
// 定义接口
type Shape interface {
Area() float64
Perimeter() float64
}任何类型,只要实现了这两个方法,就自动满足这个接口——不需要写 implements Shape,隐式满足:
type Rectangle struct {
Width float64
Height float64
}
// Rectangle 实现 Area 方法
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Rectangle 实现 Perimeter 方法
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// 现在 Rectangle 自动满足 Shape 接口,可以赋值给 Shape 变量
var s Shape = Rectangle{3, 4} // 隐式满足,编译通过
fmt.Println(s.Area()) // 12鸭子类型:Go 接口是鸭子类型——"走起来像鸭子、叫起来像鸭子,就是鸭子"。只要你有接口要求的所有方法,就满足接口。不需要声明。
二、接口的用法:多态
用接口可以写出"依赖抽象,不依赖具体"的代码:
func PrintArea(s Shape) {
fmt.Printf("Area is %.2f\n", s.Area())
}
// 传 Rectangle 可以
PrintArea(Rectangle{3, 4})
// 如果以后加了 Circle,只要 Circle 实现 Area,就能直接用
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
PrintArea(Circle{5}) // 不用改 PrintArea,直接用这就是多态:同一个接口变量,可以放不同的具体类型,调用同一个方法,实际执行不同的代码。
三、空接口 interface{}
空接口没有方法,所以任何类型都满足空接口:
var i interface{}
i = 42 // int 可以
i = "hello" // string 可以
i = User{} // 自定义类型也可以要存"任意类型"的时候用空接口,比如:
- 函数接收任意类型的参数
- 做任意类型的容器(比如
[]interface{})
从空接口里拿值,用类型断言:
i = 42
v, ok := i.(int) // 类型断言,判断是不是 int
if ok {
fmt.Println("it's int:", v)
}类型断言下一篇讲。
四、最大的坑:nil 接口 vs nil 指针
这是 Go 最容易踩的坑:nil 接口 != nil 指针。
type User struct {
Name string
}
var u *User = nil // u 是 nil 指针
var i interface{} = u
fmt.Println(i == nil) // 输出什么?false!不是 true!为什么?因为接口底层存两个东西:
- 类型:当前放的是什么类型
- 值:当前放的值
所以 i interface{} = u:
- 类型:
*User(不是 nil) - 值:nil(指针是 nil)
所以 i == nil 只有当类型和值都是 nil 的时候才是 true。只要类型不是 nil,即使值是 nil,整个接口也不是 nil。
坑在哪里?通常写代码:
func findUser(id int) (*User, error) {
if id < 0 {
return nil, fmt.Errorf("invalid id")
}
return &User{"Alice"}, nil
}
// 错了!错误写法
var err error = findUser(-1)
if err != nil {
// 处理错误
}
// 这里 err != nil 永远是 true,即使返回 nil 指针给接口正确做法:如果返回接口,就直接返回接口值,不要把 nil 指针赋给接口。记住:nil 指针不等于 nil 接口。
五、接口值的内存结构(简化理解)
type iface struct {
tab *itab // 类型信息
data unsafe.Pointer // 指向实际数据
}只有 tab(类型)和 data(数据)都是 nil 时,iface == nil 才是 true。只要有一个不是 nil,整个接口就不是 nil。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| nil 指针赋值给接口,接口不是 nil | if err != nil 永远成立 | 返回接口就直接返回 nil,不要返回 nil 指针给接口 |
| 到处用空接口 | 丢失类型信息,代码不清晰 | 确需要任意类型才用,能写具体类型就写具体类型 |
| 用接口做泛型 | Go 1.18+ 有泛型了,用泛型 | 泛型比空接口类型安全 |
相关与延伸
下一篇:类型断言与类型 switch;Go 接口实现多态,对应 OOP 领域 多态与接口;接口动态分派和虚表,进阶层会讲性能对比。
一句话总结
Go 接口:定义一组方法签名,类型实现所有方法就自动满足,不用写 implements,是鸭子类型;空接口 interface{} 可以放任何类型;最大陷阱是 nil 接口 != nil 指针——只有类型和值都是 nil,接口才是 nil;接口用来实现多态,让代码依赖抽象不依赖具体。