interface

interface 是一种类型,一种抽象的类型。

接口的本质就是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。

接口的基本实现

接口就是一个需要实现的方法列表,如下的Sayer接口(列表中只有一个say):

// Sayer 接口
type Sayer interface {
	say()
}

type dog struct {}
type cat struct {}

// dog实现了Sayer接口
func (d dog) say() {
	fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
	fmt.Println("喵喵喵")
}

func main() {
	var x Sayer // 声明一个Sayer类型的变量x
	a := cat{}  // 实例化一个cat
	b := dog{}  // 实例化一个dog
	x = a       // 可以把cat实例直接赋值给x
	x.say()     // 喵喵喵
	x = b       // 可以把dog实例直接赋值给x
	x.say()     // 汪汪汪
}

值接收者和指针接收者实现接口的区别

func main() {
	var x Sayer 
	var wangcai = dog{}  // 旺财是dog类型
	x = wangcai       	 // x可以接收dog类型
	var fugui = &dog{}   // 富贵是*dog类型
	x = fugui			 // x也可以接收*dog类型
	x.say()     
}

从上面的代码中可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

func (d *dog) say() {
	fmt.Println("汪汪汪")
}

func main() {
	var x Sayer 
	var wangcai = dog{}  // 旺财是dog类型
	x = wangcai       	 // x不可以接收dog类型,编译不通过
	var fugui = &dog{}   // 富贵是*dog类型
	x = fugui			 // x可以接收*dog类型
	x.say()  
}

此时实现Sayer接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。就分别定义Sayer接口和Mover接口,如下: Mover接口。

// Sayer 接口
type Sayer interface {
	say()
}

// Mover 接口
type Mover interface {
	move()
}

dog既可以实现Sayer接口,也可以实现Mover接口。

type dog struct {
	name string
}

// 实现Sayer接口
func (d dog) say() {
	fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
	fmt.Printf("%s会动\n", d.name)
}

func main() {
	var x Sayer
	var y Mover
	var a = dog{name: "旺财"}
	x = a
	y = a
	x.say()
	y.move()
}

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先定义一个Mover接口,它要求必须由一个move方法。

// Mover 接口
type Mover interface {
	move()
}

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
	name string
}

type car struct {
	brand string
}

// dog类型实现Mover接口
func (d dog) move() {
	fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
	fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

func main() {
	var x Mover
	var a = dog{name: "旺财"}
	var b = car{brand: "保时捷"}
	x = a
	x.move()
	x = b
	x.move()
}

上面的代码执行结果如下:

旺财会跑
保时捷速度70迈

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现(类似于继承)。

// WashingMachine 洗衣机
type WashingMachine interface {
	wash()
	dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
	fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
	dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
	fmt.Println("洗刷刷")
}

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interface {
	say()
}

// Mover 接口
type Mover interface {
	move()
}

// 接口嵌套
type animal interface {
	Sayer
	Mover
}

嵌套得到的接口的使用与普通接口一样,下面让cat实现animal接口:

type cat struct {
	name string
}

func (c cat) say() {
	fmt.Println("喵喵喵")
}

func (c cat) move() {
	fmt.Println("猫会动")
}

func main() {
	var x animal
	x = cat{name: "花花"}
	x.move()
	x.say()
}

空接口

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

func main() {
	// 定义一个空接口x
	var x interface{}
	s := "Hello 沙河"
	x = s
	fmt.Printf("type:%T value:%v\n", x, x)
	i := 100
	x = i
	fmt.Printf("type:%T value:%v\n", x, x)
	b := true
	x = b
	fmt.Printf("type:%T value:%v\n", x, x)
}

空接口可以作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
	fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "沙河娜扎"
	studentInfo["age"] = 18
	studentInfo["married"] = false
	fmt.Println(studentInfo)

类型元数据

不管是内置类型,还是自定义类型,都有对应的类型描述信息,这就是类型元数据。而且每种类型元数据都是全局唯一的(包括所有内建类型和自定义类型),这些类型元数据共同构成了Go语言的类型系统。

类型元数据对应在源码中即是_type,含义是concrete type,即实际类型描述信息。每个类型元数据基础信息都是runtime._type结构体,作为每个类型元数据的Header,所有变量类型都是以_type为内嵌字段封装而成的结构体,编译器和运行时可以根据该元信息解析具体类型、类型名存放的位置、类型的 Hash 值等基本信息。

type _type struct {
    size       uintptr //大小
    ptrdata    uintptr //含有所有指针类型前缀大小
    hash       uint32  //类型
    tflag      tflag //类型的特征标记
    align      uint8 //作为整体变量存放时的对齐字节数
    fieldalign uint8 //当前结构字段的对齐字节数
    kind       uint8 //基础类型枚举值和反射中的 Kind 一致,kind 决定了如何解析该类型
    alg        *typeAlg //指向一个函数指针表,该表有两个函数,一个是计算类型 Hash 函数。另一个是比较两个类型是否相同的 equal 函数
    gcdata     *byte //GC 相关
    str        nameOff  //类型名称字符串在二进制文件段中的偏移量
    ptrToThis  typeOff //类型元信息指针在二进制文件段中的偏移量
}

变量包括(type, value)两部分,这2个部分都相等,才能判断为相等,理解这一点就知道为什么有时候nil != nil了。

而 type 包括 <static type, concrete type>。static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型。

slice为例了解运行时类型系统的设计

type slicetype struct {
	typ  _type
	elem *_type
}

第一个typ变量_type是本slice的基本类型元数据,elem 变量 *_type 指向其存储的元素的类型元数据,如果是string类型的slice,这个指针就指向string类型的元数据。

自定义类型

自定义类型会多一个uncommontype结构体:

type uncommontype struct {
	pkgpath nameOff
	mcount  uint16 // number of methods
	xcount  uint16 // number of exported methods
	moff    uint32 // offset from this uncommontype to [mcount]method
	_       uint32 // unused
}

利用类型元数据来解释下面两种写法的区别:

  • 取别名:type MyType1 = int32

  • 自定义类型:type MyType2 int32

MyType1和int32会关联到同一个类型元数据,属于同一种类型,rune和int32就是这样的关系。

MyType2这种写法属于基于已有类型创建新类型,MyType2会自立门户,拥有自己的类型元数据,会多一个uncommontype结构体,即使MyType2相对于int32来说没有做任何改变,他们两个对应的类型元数据也已经不同了。

接口底层实现原理

根据 interface 是否包含有 method,底层实现上用两种 struct 来表示:iface 和 eface。eface 含义是 empty interface,表示不含 method 的 interface 结构。iface 至少包含一个 method,method 的具体实现存放在 itab.fun 变量里。

type eface struct {  
 _type *_type  // 具体类型(concrete type),即上文中的类型元数据
 data  unsafe.Pointer  // 具体类型的值
}  

type iface struct {  
 tab  *itab  
 data unsafe.Pointer  
}  

itab

type itab struct {  
 inter  *interfacetype  
 _type  *_type  
 link   *itab  
 bad    int32  
 inhash int32      
 fun    [1]uintptr // 存的是接口函数的具体实现
}

// interfacetype 存储 interface 本身的信息
type interfacetype struct {  
 typ     _type  
 pkgpath name  
 mhdr    []imethod  
}  
  
//imethod 存储的只是函数声明,比如  func Print() error
type imethod struct {     
 name nameOff  
 ityp typeOff  
}  

类型断言

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)

其中:

  • x:表示类型为interface{}的变量

  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

func main() {
	var x interface{}
	x = "Hello 沙河"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("类型断言失败")
	}
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候可以使用switch语句来实现:

func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type!")
	}
}

类型断言的原理

Go语言里每种类型的类型元数据都是全局唯一的(包括所有内建类型和自定义类型),以分析 non-empty 类型的 interface 为例,其 itab 指针是用<interface type, concrete type>作为key做全局缓存的,因此判断接口变量的动态类型,只需要取出该变量元数据指针,看是否指向目标全局类型元数据指针即可。也就是说,类型断言能否成功,取决于变量的concrete type,而不是static type。

反射

反射是针对于 interface 类型来说的,静态类型在编译阶段确定其具体类型,但当变量为 interface 时,只有在运行时才能确定其具体类型,反射就是允许在运行时再去读取和修改一个变量的类型信息和值信息的机制。

interface及其pair<concrete type,value>)的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来操作存储在接口变量内部pair对的一种机制。reflect.TypeOf()读取的是concrete type,reflect.ValueOf()读取的是value,可以再调用返回变量提供的Set方法来修改它的value

典型场景:json反序列化,接收的是文本数据,是不知道对应在 go 语言中是什么数据类型的,需要通过反射机制在运行时阶段来实现文本数据解析到 go 语言对应数据类型。下面是通过reflect读取字段信息的例子:

type student struct {
	Name  string `json:"name"`
	Score int    `json:"score"`
}

func main() {
	stu1 := student{
		Name:  "小王子",
		Score: 90,
	}

	t := reflect.TypeOf(stu1)
	fmt.Println(t.Name(), t.Kind()) // student struct
	// 通过for循环遍历结构体的所有字段信息,包括字段的tag
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
	}

	// 通过字段名获取指定结构体字段信息
	if scoreField, ok := t.FieldByName("Score"); ok {
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
	}
}

优缺点

反射可以大大提高程序的灵活性,使得interface有更大的发挥空间。但是:

  1. 反射中的类型错误会在运行时导致panic。

  2. 大量使用反射的代码通常难以理解。

  3. 降低了代码的运行效率,因为反射出变量的类型需要额外开销。

Last updated

Was this helpful?