接口
需要预先了解:
1. 接口
**接口(interface)**只描述"能做什么",不关心"是什么"。调用方依赖接口,具体实现可以是任意类型——这就是把"变化的实现"和"稳定的调用逻辑"隔离开的方式。Go 的接口有两个特点:实现是隐式的,不需要显式声明;接口值本身由"动态类型 + 动态值"两部分组成,后者是理解 nil 接口陷阱的关键。
1.1 从需求引入
假设要打印一批图形的面积。如果函数参数写成具体类型,每加一种图形就要改一次函数签名:
func printArea(r Rect) { /* ... */ }
func printArea2(c Circle) { /* ... */ }
调用方其实只关心"这个对象能不能算出面积"。把这件事抽象出来:
type Shape interface {
Area() float64
Name() string
}
任何拥有这两个方法的类型都自动满足 Shape,函数只需要接收 Shape:
func printArea(s Shape) {
fmt.Printf("%s 面积 %.2f\n", s.Name(), s.Area())
}
1.2 定义与隐式实现
接口定义就是一组方法签名:
type Shape interface {
Area() float64
Name() string
}
实现一个接口不需要写任何"声明",只要类型拥有这些方法:
type Rect struct {
W, H float64
}
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Name() string { return "矩形" }
type Circle struct {
R float64
}
func (c Circle) Area() float64 { return 3.14159 * c.R * c.R }
func (c Circle) Name() string { return "圆" }
Rect 和 Circle 都没有提到 Shape,但它们都满足它:
shapes := []Shape{Rect{3, 4}, Circle{1}}
for _, s := range shapes {
fmt.Printf("%s 面积 %.2f\n", s.Name(), s.Area())
}
矩形 面积 12.00
圆 面积 3.14
隐式实现的好处是:接口可以由使用方事后定义,不需要修改已有类型。标准库里的 fmt.Stringer、io.Reader 都是这样——你的类型只要方法签名对得上,就能直接用在这些 API 上。
编译期验证实现关系
隐式实现没有声明可以看,容易在重构时悄悄破坏。惯用的检查方式是给一个空标识符赋值:
var _ Shape = (*Rect)(nil)
这行的意思是:把 (*Rect)(nil) 赋给 Shape 类型的变量。如果 *Rect 不再满足 Shape,编译就会失败。把它放在类型定义附近,重构时能第一时间发现问题。
1.3 接口值的组成
接口变量不是简单的"引用",它由两部分组成:
var s Shape = Rect{W: 3, H: 4}
s(接口值)
+--------------+
| 动态类型 | ---------> Rect 的类型信息
+--------------+
| 动态值 | ---------> {3, 4}(一份副本)
+--------------+
- 动态类型:赋给接口的具体类型,这里是
Rect; - 动态值:具体值的一份拷贝,这里是
Rect{3, 4}。
用反射可以看到这两部分:
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
fmt.Println("动态类型:", t, "Kind:", t.Kind(), "动态值 Kind:", v.Kind())
fmt.Printf("格式化: %v %T\n", s, s)
动态类型: main.Rect Kind: struct 动态值 Kind: struct
格式化: {3 4} main.Rect
%T 打印动态类型,%v 打印动态值。接口值的内存结构(eface / iface)见 接口与 channel 的底层实现。
1.4 nil 接口陷阱
接口为 nil 的条件是动态类型和动态值都为空。如果动态类型存在,即使动态值是一个 nil 指针,接口也不等于 nil:
type MyErr struct{}
func (e *MyErr) Error() string { return "boom" }
func returnsError() error {
var p *MyErr // nil 指针
return p // 装进接口后,动态类型是 *MyErr
}
func returnsNil() error {
return nil
}
err := returnsError()
fmt.Println("err == nil:", err == nil) // false
fmt.Printf("err 的值: %v,动态类型: %T\n", err, err) // boom,*main.MyErr
fmt.Println("returnsNil() == nil:", returnsNil() == nil) // true
returnsError 明明返回的是一个 nil 指针,err == nil 却是 false。这就是"返回接口类型 + 返回 nil 指针"的经典陷阱:调用方写 if err != nil 会误判。
规避方法有两条:
- 直接返回
nil,不要让 nil 指针经过接口这一层; - 函数返回具体类型
*MyErr,由调用方决定是否装进接口。
同样的陷阱也出现在其他接口类型上,比如把 (*bytes.Buffer)(nil) 赋给 io.Reader。
注意:
err不为 nil 时打印它仍会调用Error()方法,因为 nil 指针作为接收者时,只要方法体不解引用它就能正常工作。
1.5 类型断言
类型断言从接口值里取出具体类型的值:
var s Shape = Rect{1, 2}
r, ok := s.(Rect)
fmt.Println("断言成 Rect:", r, ok) // {1 2} true
c, ok := s.(Circle)
fmt.Println("断言成 Circle:", c, ok) // {0} false
带 ok 的形式永远安全,断言失败时 ok 为 false,c 是该类型的零值。
不带 ok 的形式在失败时 panic:
c := s.(Circle)
panic: interface conversion: main.Shape is main.Rect, not main.Circle
只有在"确定类型就是它"的场合才用不带 ok 的形式。
1.6 类型 switch
类型断言一次只能试一种类型,多类型分支用类型 switch:
switch v := s.(type) {
case Rect:
fmt.Println("命中矩形:", v.W, v.H)
case Circle:
fmt.Println("命中圆:", v.R)
default:
fmt.Println("未知类型")
}
每个 case 里的 v 已经是具体类型(不是接口),可以直接访问字段。一个 case 可以列出多个类型,此时 v 保持接口类型:
switch v := s.(type) {
case Rect, Circle:
fmt.Println("是图形之一:", v.Name())
}
1.7 空接口与 any
空接口没有方法,因此所有类型都满足它:
var a any = 42
var b any = "文本"
var c interface{} = 3.14
any 是 interface{} 的别名(Go 1.18+),两者完全等价。空接口适合做"任意类型"的容器,代价是取出来时必须断言或反射:
values := []any{1, "a", true, Point{1, 2}}
for _, v := range values {
fmt.Printf("%v 的类型是 %T\n", v, v)
}
1 的类型是 int
a 的类型是 string
true 的类型是 bool
(1, 2) 的类型是 main.Point
注:能用具体类型或泛型表达时,尽量不要用
any。空接口会把类型检查推迟到运行时,而且会让值逃逸到堆上。泛型的写法见 泛型。
1.8 标准库里的常见接口
| 接口 | 方法 | 用途 |
|---|---|---|
fmt.Stringer |
String() string |
自定义 %v/%s 的输出 |
error |
Error() string |
表示一个错误(详见 错误处理) |
sort.Interface |
Len()、Less(i, j)、Swap(i, j) |
自定义排序 |
io.Reader / io.Writer |
Read(p []byte) / Write(p []byte) |
统一的读写抽象(详见 标准库(一):IO 与系统交互) |
fmt.Stringer
实现 String() 方法后,fmt 打印时会调用它:
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
p := Point{1, 2}
fmt.Println(p) // (1, 2)
fmt.Printf("%v %s\n", p, p) // (1, 2) (1, 2)
sort.Interface
排序包不关心元素是什么类型,只要求三个方法:
type User struct {
Name string
Age int
}
type ByAge []User
func (u ByAge) Len() int { return len(u) }
func (u ByAge) Less(i, j int) bool { return u[i].Age < u[j].Age }
func (u ByAge) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
users := []User{{"甲", 30}, {"乙", 22}, {"丙", 27}}
sort.Sort(ByAge(users))
fmt.Println(users) // [{乙 22} {丙 27} {甲 30}]
1.9 接口组合与小接口哲学
接口可以嵌入其他接口,把能力拼起来:
type Reader interface {
Read() string
}
type Writer interface {
Write(s string)
}
type ReadWriter interface {
Reader
Writer
}
标准库的 io.ReadWriter、io.ReadWriteCloser 都是这样组合出来的。
Go 社区的经验是接口要小:
- 一个接口一两个方法最合适,
io.Reader只有一个Read; - 接口由使用方定义,而不是由实现方定义。实现方只需要提供具体类型,使用方按自己需要的方法集定义接口,这样接口不会带着一堆用不到的方法。
// 使用方只需要 Read,就只定义 Read
type stringReader interface {
Read() string
}
func consume(r stringReader) string { return r.Read() }
1.10 值实现 vs 指针实现
一个类型是否满足接口,取决于方法集(见 方法):
- 值接收者方法:
T和*T都有; - 指针接收者方法:只有
*T有。
所以用指针接收者实现接口时,只有指针类型满足接口:
type Person struct{ Name string }
func (p *Person) Greet() string { return "你好,我是 " + p.Name }
var g Greeter = &Person{Name: "一撮灰"} // 可以
var g2 Greeter = Person{Name: "灰灰"} // 编译错误
cannot use Person{…} (value of struct type Person) as Greeter value in variable declaration:
Person does not implement Greeter (method Greet has pointer receiver)
反过来,如果方法用的是值接收者,那么 T 和 *T 都满足接口,两种写法都行。
注:接口里存的是值的副本。把大结构体装进接口会触发一次拷贝,还可能让它逃逸到堆上。频繁通过接口传递大对象时,考虑存指针。
1.11 练习
- 定义
Animal接口(Sound() string),实现Dog、Cat两个类型,写一个函数接收[]Animal并打印每种动物的叫声。 - 故意写一个返回
error但内部返回 nil 指针的函数,验证接口不等于 nil,然后改成直接返回nil。 - 给一个
User类型实现fmt.Stringer和sort.Interface,分别验证fmt.Println与sort.Sort的效果。



