interface
[TOC]
接口
接口,在计算机系统往往作为多个系统之间沟通的一种契约,是交互双方共同遵守的规则。通过接口可以:
- 抽象,规范的作用
- 减少依赖,隐藏内部实现细节
- 系统之间解耦,提升可移植性
- 预留扩展空间
Go interface{}
在 Go 语言中的接口是一组方法的签名的集合,是一种内置的类型。
Go 的接口不像其他语言需要通过类似 implements
来显式的语义来表达。 在 Go 中,只要目标类型方法集包含接口声明的全部方法,就可认为实现了接口,目标类型也可以同时实现多个接口。完全非侵入设计。对于这种非侵入的设计,有不同的声音,有人觉得好,也有人觉得不够明确,看起来也不方便。
接口本身也是一种结构类型,但有一些限制。
- 接口中不能有字段
- 只能声明方法,不能实现
- 可嵌入其他接口类型
在实现上,根据是否包含方法,Go 接口主要分为两种:
- 使用
runtime.iface
结构体表示包含方法的接口; - 使用
runtime.eface
结构体表示不包含任何方法的空接口interface{}
类型,与 C 语言中的void *
不同,interface{}
类型不是任意类型。如果我们将类型转换成了interface{}
类型,变量在运行期间获取变量类型是interface{}
。
// 带方法的接口
type Writer interface {
Write(p []byte) (n int, err error)
}
//不带函数的空interface
var empty interface{}
接口实现
结构体实现与指针实现
接口在定义一组方法时没有对实现的接收者做限制,所以我们会看到某个类型实现接口的两种方式:
type Animal interface {
Walk()
}
// 值接收者
type Dog struct {}
func (d Dog) Walk() {}
// 指针接收者
type Cat struct {}
func (c *Cat) Walk() {}
在实现接口的时候,可分为值和指针,在初始化时也可以初始化成结构体或者指针。根据接口实现和初始化阶段是结构体还是指针可以有4中组合,但是只有3 中能通过编译:
结构体实现接口 | 结构体指针实现接口 | |
---|---|---|
结构体初始化变量 | 通过 | 不通过 |
结构体指针初始化变量 | 通过 | 通过 |
也就是下面这种情况无法通过编译:
type Animal interface {
Walk()
}
// 值接收者
type Dog struct {}
func (d Dog) Walk() {}
// 指针接收者
type Cat struct {}
func (c *Cat) Walk() {}
func main() {
var a Animal
a = Dog{} //可以编译
a = &Dog{} //可以编译
a = &Cat{} //可以编译
a = Cat{} //无法通过编译
a.Walk()
}
我们知道,Go 中所有的传递都是值传递,
Dog 是采用结构体实现了接口,初始化时,传 &Dog{} 和 Dog{}
都是可以的,在传指针的 &Dog{}
变量时,起始隐式地获取到指向的结构体。
使用 &Cat{}
指针初始化赋值时,也发生了拷贝,但这个指针与原来的指针指向相同结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体;
而在传Cat{}
时,完全赋值了一个新的结构体对象,指向的已经不是原来的对象了。
使用的区别
type chameleon interface {
ChangeColor(color string)
ShowColor()
}
// 值接收者
type Cl struct {
color string
}
func (c Cl) ChangeColor(color string) {c.color = color}
func (c Cl) ShowColor() {fmt.Println(c.color)}
// 指针接收者
type Cl2 struct {
color string
}
func (c *Cl2) ChangeColor(color string) {c.color = color}
func (c *Cl2) ShowColor() {fmt.Println(c.color)}
调用
func TestCl_ChangeColor(t *testing.T) {
var c chameleon
cl := Cl{color:"white"}
c = cl // 改为 c = &cl 也一样的结构
c.ChangeColor("gray") // 修改的是新的副本,不影响调用者 cl
c.ShowColor() // white
fmt.Println(cl.color) // white
}
func TestCl2_ChangeColor(t *testing.T) {
var c chameleon
cl := Cl2{color:"white"}
c = &cl
c.ChangeColor("gray")
c.ShowColor() // gray
fmt.Println(cl.color) // gray
}
所以他们的主要区别是:
- 如果方法的接收者是值类型,无论初始化的调用者是值对象还是指针,修改的都是新的对象的副本,不影响原调用者;
- 如果方法的接收者是指针类型,则调用者(只能传指针)修改的是指针指向的对象本身。
所以上述用例1 变色龙修改为灰色,但是原来的调用者还是白色,而用例2就可以顺利的把调用者自身改为灰色。
在实际中,还是优先推荐使用指针接收者,这样可以避免一些意想不到的 bug。
iface
位于 Go SDK/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type //动态类型指针,go类型的运行时表示
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
type _type struct {
// 类型大小
size uintptr
ptrdata uintptr
// 类型的 hash 值
hash uint32
// 类型的 flag,和反射相关
tflag tflag
// 内存对齐相关
align uint8
fieldalign uint8
// 类型的编号,有bool, slice, struct 等
kind uint8
alg *typeAlg
// gc 相关
gcdata *byte
str nameOff
ptrToThis typeOff
}
_type
是 Go 语言类型的运行时表示。
eface
eface 代表了一个空 interface{}
,Go 语言中的任意类型都可以转换成 interface{}
类型。由于 interface{}
类型不包含任何方法,结构也比 iface 简单,只包含指向底层数据和类型的两个指针。
type eface struct {
_type *_type //动态类型 同上述,很多类型都有 _type
data unsafe.Pointer //数据指针
}
问题思考
为什么要分为 iface 和 eface?
-
iface
描述的接口包含方法; -
eface
则是不包含任何方法的空接口:interface{}
。
接口实现机制有运行期开销?
根据接口的实现和初始化时传值是构体还是指针会有4中组合,哪些组合无法通过编译?为什么?
接口的结构体实现和指针实现上的区别?
- 如果方法的接收者是值类型,无论初始化的调用者是值对象还是指针,修改的都是新的对象的副本,不影响原调用者;
- 如果方法的接收者是指针类型,则调用者(只能传指针)修改的是指针指向的对象本身。
方法与函数的区别?
- 相比函数,方法多了一个接收者。