接口就是一组方法
接口类型列出一组方法签名。任何拥有这些方法的类型都满足这个接口,不需要任何声明。
Rect 和 Circle 都没有提到 Shape。这就是隐式实现,也是 Go 接口最核心的特点。它意味着你可以在自己的包里定义一个接口,而其他包里的类型不用改动任何代码,就已经满足了它。
标准库中的小接口
Go 代码偏爱只有一两个方法的接口。最重要的几个:
| 接口 | 方法 | 使用者 |
|---|---|---|
fmt.Stringer | String() string | fmt 的打印函数 |
error | Error() string | 所有可能失败的函数 |
io.Reader | Read(p []byte) (n int, err error) | 文件、网络、gzip、HTTP 请求体 |
io.Writer | Write(p []byte) (n int, err error) | 文件、缓冲区、哈希、HTTP 响应 |
sort.Interface | Len、Less、Swap | sort 包 |
http.Handler | ServeHTTP(w, r) | net/http |
因为 io.Reader 只有一个方法,几十种类型都实现了它,任何接收 io.Reader 的函数都能处理所有这些类型:
Go 有一句格言:“接口越大,抽象越弱”(the bigger the interface, the weaker the abstraction)。更大的接口是由小接口组合而成的:io.ReadWriter 就是 Reader 加上 Writer,通过在一个接口中嵌入另一个接口来实现。
any:空接口
interface{} 没有任何方法,所以所有类型都满足它。Go 1.18 新增了别名 any,两者完全相同。
any 值可以装下任何东西,但在你用类型断言或类型 switch 取回具体类型之前,几乎什么都做不了。如果类型的范围是已知的,优先使用真正的接口或泛型。any 适合真正动态的数据,比如结构未知的 JSON 解码结果,以及打印。
接口值里装着什么
接口值是一对数据:动态类型和动态值。var s Shape = Rect{3, 4} 保存了类型 Rect 和这个值的一份副本。调用 s.Area() 时,会在运行时查找 Rect 的方法。
只有当两部分都为空时,接口才是 nil。这条规则导致了 Go 里最让人困惑的 bug。
nil 接口陷阱
把一个 nil 指针存进接口,得到的是一个非 nil 的接口。
输出:
false
*main.MyError true
true
validate(true) 返回的 error 接口装着类型 *MyError 和值 nil。这个接口有类型,所以它不等于 nil,调用者的 if err != nil 分支就会执行。如果在那里调用 err.Error(),就会因为访问 nil 接收者的字段而 panic。
修复方法很简单:把变量声明为 error,而不是具体的指针类型,或者在成功路径上直接返回字面量 nil。永远不要在返回值为 error 的函数里返回具体的错误指针类型。同样的陷阱适用于所有接口,不只是错误。
检查类型是否实现了接口
只有在把值赋给接口的地方,才会检查是否实现了接口。如果还没有代码这样做,方法签名写错了也不会被发现。在包级别加一个空白赋值,就能让这个检查明确发生:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
这在运行时没有任何开销。如果 *LogWriter 的方法是 Write(p []byte) error,而不是 Write(p []byte) (int, error),构建就会失败:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
指针接收者与接口
如果一个方法使用指针接收者,那么只有指针类型拥有这个方法。*Counter 能借此满足接口,Counter 却不能。编译器会提示 Counter does not implement Incrementer (method Inc has pointer receiver)。请把 &Counter{} 存进接口。方法页面讲解了方法集。
接收接口,返回结构体
Go 中一条常见的准则是:函数应该接收接口参数,并返回具体类型。
- 接收接口让调用者可以传入任何符合要求的东西,包括测试用的假对象。一个读取数据的函数应该接收
io.Reader,而不是*os.File。 - 返回具体类型让调用者可以使用它的所有方法和字段,也避开了 nil 接口陷阱。
os.Open返回的是*os.File,而不是io.Reader。
一个相关的习惯是:在使用接口的地方定义接口,而不是在实现它的地方。如果你的服务需要一个能 Get(id) 获取用户的东西,就在服务的包里声明一个只有一个方法的接口,数据库包只需导出它的结构体。
比较接口值
当两个接口值的动态类型相同、动态值相等时,它们才相等。如果动态类型不可比较(切片、map),== 能编译通过,但会在运行时 panic:runtime error: comparing uncomparable type []int。
常见错误
- 把带类型的 nil 指针作为接口返回。 请返回字面量
nil。 - 过早引入接口。 先写具体类型。等到出现第二种实现或者测试需要时,再添加接口。
- 指向接口的指针。
*io.Reader几乎从来都是错的。当你把指针存进接口时,接口本身就已经装着这个指针了。 - 大接口。 有十个方法的接口既难实现,又难伪造。把它拆开。
常见问题
Go 语言怎么实现一个接口?
在你的类型上定义接口列出的那些方法,名字和签名都要一致。Go 没有 implements 关键字。如果 *File 有 Read(p []byte) (int, error) 方法,它就自动成为一个 io.Reader。在你把值赋给接口类型的地方,编译器都会做这个检查。
Go 里的空接口或 any 是什么?
interface{} 没有任何方法,所以所有类型都满足它。从 Go 1.18 起,any 是 interface{} 的内置别名。any 类型的值可以装下任何东西,但要取回具体类型,你需要类型断言或类型 switch。
为什么我把 nil 指针赋给 Go 接口后,接口却不是 nil?
接口值包含一个类型和一个值。把一个为 nil 的 *MyError 赋给 error,得到的接口类型是 *MyError、值是 nil,而这个接口并不等于 nil。没有错误时,请直接返回字面量 nil,而不是带类型的 nil 指针。
怎么在编译期检查一个类型是否实现了某个接口?
在包级别加一个空白赋值:var _ io.Reader = (*MyReader)(nil)。如果 *MyReader 缺少某个方法,构建就会失败,报错信息会写出缺少的方法名。