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中组合,哪些组合无法通过编译?为什么?

接口的结构体实现和指针实现上的区别?

  • 如果方法的接收者是值类型,无论初始化的调用者是值对象还是指针,修改的都是新的对象的副本,不影响原调用者;
  • 如果方法的接收者是指针类型,则调用者(只能传指针)修改的是指针指向的对象本身。

方法与函数的区别?

  • 相比函数,方法多了一个接收者。

更新时间: