问题引入
众所周知,Go 是一款静态强类型的编程语言。在 Go 中想要一个结构实现一个接口只需要实现这个接口里面的所有方法即可。举个栗子:
定义 Animal
结构体如下:
go
type Animal struct {
Name string
}
然后定义 Creature
接口:
go
type Creature interface {
Move()
Eat()
Bark()
}
如果 Animal
想要实现 Creature
接口,那么只需要这么写:
go
func (a *Animal) Move() {
//TODO implement me
panic("implement me")
}
func (a *Animal) Eat() {
//TODO implement me
panic("implement me")
}
func (a *Animal) Bark() {
//TODO implement me
panic("implement me")
}
但是问题来了,Go 程序里面没有类似 implements
的关键字,那么如何知道当前结构体实现了这个接口里面的所有方法呢?
最直接的方法是依靠 IDE 的提示功能------编辑器最左侧的"绿色箭头",但是这样治标不治本,现在给大家介绍一种很简单的方法。
最简单的方法
核心思路是:
- 创建一个接口类型的变量。
- 把你的结构体实例赋值给它。
- 如果结构体没有实现该接口,编译器会直接报错。
添加如下语句:
go
// 静态检查:Animal 必须实现 Creature 接口
var _ Creature = (*Animal)(nil)
简单来说就是创建了一个匿名的 Creature
类型变量,因为这里使用了 _
符号,所以并不占用内存。最终将 nil
强转为 Creature
类型。
每次你给接口添加了新的方法之后,都会触发这行代码的静态检查,这样就不会遗漏。
引用类型和指针之间的关系
下面再来说说为什么可以像上面那样写,左边是接口类型,右边是指针类型不是同一个类别(Kind)啊。这里就得要牵扯到 Go 程序中引用类型和指针的关系了。
指针类型(Pointer)
定义 :指针类型是显式的 *T
,表示指向某个类型 T
的内存地址。
特征:
- 需要用
&
获取地址,用*
解引用。 - 指针本身是一个值,存储的是另一个变量的内存地址。
- 传递指针可以避免复制大对象。
e.g
go
x := 10
p := &x // p 是 *int,指向 x
fmt.Println(*p) // 输出 10
引用类型(Reference)
定义:Go 中的某些类型本身就是「引用语义」,赋值或传参时不会复制底层数据,而是复制一个"指向底层数据的描述符"。
常见的引用类型:
slice
map
chan
function
interface
特征:
- 本身是一个"头部结构"(例如 slice 有 len、cap、ptr),底层数据存放在堆或共享区域。
- 拷贝它们时只是复制了头部,多个变量会共享底层数据。
e.g
go
s1 := []int{1, 2, 3}
s2 := s1 // s1 和 s2 引用相同底层数组
s2[0] = 99
fmt.Println(s1) // [99 2 3]
区别与关系
区别:
- 指针类型:显式使用
*T
,操作的是地址; - 引用类型:语法上看不到
*
,但赋值/传参时会共享底层数据。
关系:
- 引用类型的"头部"在传递时其实就是值语义,但头部里保存了一个指针,指向底层数据。
- 所以可以说:引用类型的本质就是内部持有一个或多个指针的结构体。
为什么可以转为接口类型
接口的底层表示
Go 的接口变量在运行时由两部分组成(可以理解为一个结构体),在 src/runtime/runtime2.go
文件中可以查看到接口的元定义:
go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
iface
→ 非空接口(有方法的接口)的内部表示eface
→ 空接口(interface{}
)的内部表示
src/runtime/type.go
定义了 itab
、_type
等结构体,用于保存类型元信息和方法表:
go
type itab struct {
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr // 实际是变长,存储方法实现指针
}
为什么可以强行赋值?
因为 Go 规定:
- 如果某个 类型
T
或*T
实现了接口方法集,就可以把它赋值给接口变量。 - 赋值时,接口里的
data
就会存放一个指针(可能是*T
,也可能是一个复制的T
值)。
关键点:方法集规则
Go 有个 方法集(Method Set) 概念,决定了 T
和 *T
分别实现了哪些方法:
- 类型
T
的方法集:包括所有值接收者的方法。 - 类型
*T
的方法集:包括所有值接收者和指针接收者的方法。
所以:
- 如果接口方法是值接收者实现的,
T
和*T
都能赋值给接口。 - 如果接口方法是指针接收者实现的,就必须用
*T
才能赋值给接口。
因为接口赋值时,并不关心你传的是值还是指针,只要该类型的方法集满足接口需求即可。
当你写:
go
var _ Creature = (*Animal)(nil)
(*Animal)(nil)
是*Animal
类型。- 编译器会检查
*Animal
的方法集是否实现了Creature
。 - 如果实现了,编译通过,接口的
data
域会保存这个指针。