需要预先了解:


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 会误判。

规避方法有两条:

  1. 直接返回 nil,不要让 nil 指针经过接口这一层;
  2. 函数返回具体类型 *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 练习

  1. 定义 Animal 接口(Sound() string),实现 Dog、Cat 两个类型,写一个函数接收 []Animal 并打印每种动物的叫声。
  2. 故意写一个返回 error 但内部返回 nil 指针的函数,验证接口不等于 nil,然后改成直接返回 nil。
  3. 给一个 User 类型实现 fmt.Stringer 和 sort.Interface,分别验证 fmt.Println 与 sort.Sort 的效果。