Golang 06_Golang接口

一、接口(interface)简介

1.1 接口的概念

多数情况下,数据可能包含不同的类型,却会有一个或者多个共同点,这些共同点就是抽象的基础。

在Go中,接口(interface) 是一种抽象类型,它没有属性,用于定义对象的行为规范,是仅包含一组方法(行为)签名(方法名、参数、返回值)、未具体实现方法的集合。

接口可以理解为某一个/些方面的抽象,可以是多对一的(多个类型实现一个接口),这也是多态的体现。

接口(interface)定义了一类对象的行为(方法)规范(输入、输出参数),只定义规范不实现,由具体的对象来实现行为(函数体)的细节。

Golang中,接口本身也是一种类型,它代表的是一个方法的集合。任何类型只要实现了接口中声明的所有方法,那么该类型就实现了该接口。与其它语言不同,Golang并不需要显示声明类型实现了某个接口,而是由编译器和runtime进行检查。

Golang接口类型变量内部实际上包含两个字段:类型

  • 类型字段 指定了被接口变量持有的值的实际类型;
  • 值字段 则包含了被持有值的副本;

Tips: 接口是一个或多个方法签名的集合。 接口命名习惯以 er 结尾。 接口只有方法声明,没有方法实现,没有数据字段。换句话说,接口就是一个需要实现的方法列表。 接口可以匿名嵌入到其它 接口 或 结构中。 接口同样支持匿名字段方法。 对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。 只有当接口存储的类型和对象都为nil时,接口才等于nil。 接口调用不会做receiver的自动转换。 接口也可实现类似OOP中的多态。 空接口可以作为任何类型数据的容器。

任何类型的方法集中只要拥有一个接口对应的全部方法签名,就表示它 “实现” 了该接口,无须在该类型上显式声明实现了哪个接口,这称为 Structural Typing。

所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。当然,该类型还可以有其它方法。

一个类型可实现多个接口。

1
2
3
4
5
type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    
} 

接口名: 使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如:有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer 等。接口名最好要能突出该接口的类型含义。

方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省。

二、接口的实现及使用

2.1 接口的实现

一种类型只实现了一个接口中的要全部方法,那么就实现了这个接口,换句话说,接口就是一个需要实现的方法列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 定义 Sayer 接口
type Sayer interface {
    say()
}

// 只要实现了接口中的所有方法,就实现了这个接口。
type Dog struct {}
func (d Dog) say() {    // Dog实现了Sayer接口
    fmt.Println("汪汪汪")
}

type Cat struct {
    Name string
} 
func (c Cat) say() {    // Cat实现了Sayer接口
    fmt.Println("喵喵喵")
} 

如何测试一种类型是否已实现该接口? 使用接口特有的断言来判断,语法:

1
x.(T)

T 为要判断的类型名,该语法只适应于x是interface类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 由于x.(T)只能是接口类型判断,所以传参时候,传入的是接口类型
// 为何test的类型可以是一个空接口?埋伏笔下文便知。
func CheckSayer(test interface{}) {
    if _, ok := test.(Cat); ok {
        fmt.Printf("Cat implements Sayer")
    }
}

func main() {
    c := Cat{"小花"}
    CheckSayer(c) // Cat implements Sayer
}

2.2 接口类型变量

interface 类型变量是一个引用类型,如果没有对 interface 初始化就使用,那么会输出 nil。 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

type NilStruct struct {
}

// 调用 NilOrNot 函数时 会发生隐式的类型转换,
func NilOrNot(v interface{}) bool {
    // 转换后的变量不仅包含转换前的变量,还包含变量的类型信息 NilStruct,所以转换后的变量与 nil 不相等。
    if v == nil {  // 形参 被实参赋值,值不再为空
        return true
    }
    return false
}

func main() {
    var ni interface{}  // 未初始化,值为nil
    var nt *NilStruct   // 指针未赋值,值也为nil

    fmt.Println(ni == nil)
    fmt.Println(nt == nil)
    ni = nt             // 接口赋值后,值不再为空;变量的赋值也会触发隐式类型转换
                        // *NilStruct 类型会转换成 interface{} 类型
                        // 转换后的变量不仅包含转换前的变量,还包含变量的类型信息 NilStruct,
                        // 所以转换后的变量与 nil 不相等。

    fmt.Println(ni == nil)
    fmt.Println(nt == nil)

    // NilOrNot函数调用时发生了隐式的类型转换,*NilStruct 类型会转换成 interface{} 类型
    fmt.Println(NilOrNot(nt)) 
}

// 输出:
// true
// true
// false
// true
// false

接口类型变量能够存储(引用)所有实现了该接口的实例。

1
2
3
4
5
6
7
8
9
func main() {
    var x Sayer // 声明一个Sayer类型的变量x, 值为nil,接口的零值是nil
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
} 

一个接口变量的值(简称接口值)是由一个 具体类型具体类型的值 两部分组成的,这两部分分别称为接口的 动态类型动态值

来看一个具体的例子:

1
2
3
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)

看下图分解: alt 属性文本

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type Mover interface {
    move()
}

type Dog struct {
    Name string
}
func (d Dog) move() {   // 使用值接收者实现的接口
    fmt.Printlf("%v 在奔跑\n", d.Name)
}

type Cat struct {
    name string
}
func (c *Cat) move() {  // 使用指针接收者实现的接口, 是Cat的指针(*Cat)实现了move方法,Cat本身没实现。
    fmt.Printlf("%v 在行走\n", c.Name)
}

func main() {
    var x Mover

    var dog1 = Dog{"大黄"}  // Dog类型
    x = dog1                // x可以接收dog类型
    x.move()

    var dog2 = &Dog{"二哈"}  // *Dog类型
    x = dog2                // x可以接收*Dog类型
    x.move()

    var cat = Cat{"小花"}  // Cat类型
    // x = cat             // x不可以接收Cat类型
    // cannot use cat (type CAt) as type Mover in assignment:
    // Cat does not implement Mover (move method has pointer receiver)
    x = &cat               // x可以接收*Cat类型
}

从上面的代码中我们可以发现: 1、使用值接收者实现的接口,不管是 Dog结构体 还是 *Dog 结构体指针 类型的变量都可以赋值给该接口变量,因为 Go语言中有对指针类型变量求值的语法糖*Dog 指针变量 dog2 会自动求值(解引用)。 2、使用指针接收者实现的接口, 此时实现Mover接口的是 *Cat 类型,所以不能给接口类型变量 x 传入 Cat 类型的变量 cat,此时 x 只能存储 *Cat 类型的值。

Tips: 结构体类型 和 其指针类型 是不同的类型,但是两种类型不可以同时存在同一接口方法的实现,Go 语言的编译器会在结构体类型 和 结构体指针类型 都实现同一个接口方法时报 method redeclared 错误。

在《Golang-04_Golang结构体》一章的 2.2 小节讲到,值接收者 和 指针接收者方法实现接口时的约束如下:

  • 如果定义的是值类型接收者的方法 (Type)Method,则该类型会隐式的声明一个指针类型接收者的方法 (*Type)Method
  • 如果定义的是指针类型接收者的方法 (*Type)Method,则不会隐式声明一个值类型接收者的方法 (Type)Method

在使用接口类型对象 调用方法时,涉及到接口转换,及需要把 接口类型调用者对象 转为 接收者 类型对象,所以,实现接收者是值类型的接口方法时,可以正常地用值或指针进行接口转换,但实 现接收者是指针类型的方法时,接口转换只能使用指针,不能使用值类型。

2.4 类型与接口的关系

  • 一个类型实现多个接口 一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Sayer 接口
type Sayer interface {
    say()
}
// Mover 接口
type Mover interface {
    move()
} 

type dog struct {  // dog既可以实现Sayer接口,也可以实现Mover接口。
    name string
}
func (d dog) say() {  // 实现Sayer接口
    fmt.Printf("%s会叫汪汪汪\n", d.name)
}
func (d dog) move() {  // 实现Mover接口
    fmt.Printf("%s会动\n", d.name)
}

func main() {
    var x Sayer
    var y Mover

    var a = dog{name: "旺财"}
    x = a
    x.say()
    // x.move()   // x 是 Sayer 类型的接口,没有定义 move 方法,故 x 不能调用 move

    y = a
    // y.say()  // y 是 Mover 类型的接口,没有定义 say 方法,故 y 不能调用 say
    y.move()
} 
  • 多个类型实现同一接口 Go语言中不同的类型可以实现同一接口,首先定义一个Mover接口,它要求必须由一个move方法。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Mover 接口
type Mover interface {
    move()
} 

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

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

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "保时捷"}
    x = a
    x.move()    // 旺财会跑
    
    x = b
    x.move()    // 保时捷速度70迈 
} 

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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("洗刷刷")
}

haier 类型也实现了 WashingMachine接口

2.5 接口嵌套

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 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() {
    cat := cat{name: "花花"}

    var s Sayer
    s = cat
    s.say()
    // s.move()  // s 是 Sayer 接口类型的变量,Sayer 接口没有定义 move 方法,故 s 不能调用 move

    var m Mover
    m = cat
    // m.say()  // m 是 Mover 接口类型的,Mover 接口没有定义 say 方法,故 m 不能调用 say
    m.move()

    var x animal 
    x = cat
    x.move()
    x.say()
}  

2.6 空接口(interface{})

  • 空接口的定义 空接口(interface{}) 是指没有定义任何方法的接口。

由于空接口没有任何方法,因此任何类型都实现了空接口,即可以把任何一个变量赋给空接口类型的变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
    // 定义一个空接口x
    var x interface{}

    s := "pprof.cn"
    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)
    s = stuct{
        name string
        val int
    }{"Mac", 100}
    x = s
    fmt.Printf("type:%T value:%v\n", x, x)
}
  • 空接口的应用

1、空接口作为函数的参数, 使用空接口实现可以接收任意类型的函数参数:

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

2、空接口作为map的值, 使用空接口实现可以保存任意值的字典:

1
2
3
4
5
6
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo) 

3、类型断言

空接口可以存储任意类型的值,那如何获取其存储的具体数据呢? 想要判断空接口中的值的具体类型需要使用类型断言,其语法格式:

1
value, ok := x.(T)

其中: x:表示类型为 interface{} 的变量; T:表示断言 x 的类型;

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

常用的有两种方法:Comma-ok断言switch判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import "fmt"

type Student struct { // 定义一个结构体
    Name string
}

// 类型断言示例
func main() {
    Params := make([]interface{}, 3)
    Params[0] = 88                   // 整型
    Params[1] = "咖啡色的羊驼"         // 字符串
    Params[2] = Student{Name: "cbs"} // 自定义结构体类型
    
    // Comma-ok断言
    for index, v := range Params {
        fmt.Printf("index: %v, value:%#v \n", index, v)
        if val, ok := v.(int); ok {
            fmt.Printf("Params[%d] 是int类型, value: %#v\n", index, val)
        } else if val, ok := v.(string); ok {
            fmt.Printf("Params[%d] 是字符串类型, value: %#v\n", index, val)
        } else if val, ok := v.(Student); ok {
            fmt.Printf("Params[%d] 是自定义结构体Student类型, value: %#v\n", index, val)
        } else {
            fmt.Printf("list[%d] 未知类型\n", index)
        }
    }
    
    // switch判断
    for index, v := range Params {
        fmt.Printf("index: %v, value:%#v \n", index, v)
        switch  value := v.(type) {
        case int:
            fmt.Printf("Params[%d] 是int类型, 值:%#v \n", index, value)
        case string:
            fmt.Printf("Params[%d] 是字符串类型, 值:%#v\n", index, value)
        case Student:
            fmt.Printf("Params[%d] 是自定义结构体Student类型, 值:%#v\n", index, value)
        default:
            fmt.Printf("list[%d] 未知类型\n", index)
        } 
    
    }  
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

Tips:只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

2.7 接口实现约束检查(静态检查)

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// if_service.go
package service
import "fmt"

type HelloService interface {
    Hello(msg string)
}

// main.go
package main

import "service"
type HelloServer struct{}

func (p *HelloServer) Hello(msg string) {
    fmt.Println(msg)
}

// 通过接口约束HelloService服务
// 这行代码的意思是,声明一个 service.HelloService 类型的变量,声明后这个变量的实体抛弃(不用内存来存),
// 只是做一下静态检查,让编译器把一些错误屏蔽掉,
// 核心点在(*HelloService)(nil),在编写方法的时候,如果不满足接口的实现要求就会报错,从而必须按照规范实现接口。
var _ service.HelloService = (*HelloServer)(nil)

func main() {
    var hs HelloService = &HelloServer{}
    hs.Hello("Hello world!")
}

三、interface 实现原理

参考文档:

https://zhuanlan.zhihu.com/p/60983066 https://zhuanlan.zhihu.com/p/493295775 https://zhuanlan.zhihu.com/p/425443573 https://zhuanlan.zhihu.com/p/427814919

3.1 interface 的数据结构

Golang中接口的实现结构分为两种,一种是没有任何方法定义的空接口 interface{},另外一种是有方法定义的接口,分别对应两种底层结构体实现:

  • 使用 runtime.eface 结构体表示不包含任何方法的 空接口interface{} 类型;
  • 使用 runtime.iface 结构体表示包含一个或多个方法定义的接口类型;

它们的数据结构定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// path: src/runtime/runtime2.go
// 不包含任何方法的空接口类型的底层结构体
type eface struct {
    _type *_type         // 类型元数据
    data  unsafe.Pointer // 数据信息,指向原始数据的指针
}

// 包含一个或多个法的接口类型的底层结构体
type iface struct {
    tab  *itab
    data unsafe.Pointer  // 数据信息,指向原始数据的指针
}

alt 属性文本 eface 表示空的 interface{},用两个机器字长表示,第一个机器字长 _type 是指向实际类型描述的指针,第二个机器字长 data 是指向原始数据的指针。 iface 表示至少带有一个函数的 interface,它也用两个机器字长表示,第一个机器字长 tab 是指向一个 itab结构的指针,第二个机器字长 data 是指向原始数据的指针。

data 用来保存实际变量的地址(指针) data 中的内容会根据实际情况变化,因为 Golang 在函数传参和赋值时是 值传递 的,所以: 如果实际类型是一个值,那么 interface 会保存这个值的一份拷贝,Golang 运行时会在堆上为这个值分配一块内存,然后 data 指向它。 如果实际类型是一个指针变量,那么 interface 会保存这个指针变量(存放了一个内存地址)的一份拷贝,由于 data 的长度恰好能保存这个指针的内容,所以 data 中存储的就是指针变量的值(一个内存地址),这样它(data)和实际数据(指针变量)都指向的是同一个变量。

以 interface{} 的赋值为例: alt 属性文本 上图中, i1 和 i2 是 interface{} 实例对象,A 为要赋值给 interface 的对象。

  • i1 = A 将 A 的值赋值给 i1,则 i1 中的 data 中的内容是一块新内存的地址 (0x123456),这块内存的值从 A 拷贝;
  • i2 = &A 将 A 的地址赋值给 i2,则 i2 中的 data 的值为 A 的地址,即 0xabcdef; 验证一下 interface{} 实例对象的 data 字段:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func main() {
    var val int = 100
    fmt.Printf("step 1, val: %v, type: %T, addr: %p\n", val, val, &val)

    var i1, i2 interface{}
    i1 = val    // i1 中的 data 的内容是一块新内存的地址 (0x1129c80),这块内存的值从 val 拷贝,值为 100
    i2 = &val   // i2 中的 data 的值为 val 的地址,即 0xc0000b2008, 是指向val 的指针,值为 100
    fmt.Printf("step 2, val: %#v, i1: %#v, i2: %#v, i2_data: %#v\n", val, i1, i2, *(i2.(*int)))

    val = 200   // 此处修改了val 的值为 200, 将影响到 i2, 不影响i1(值仍然为100)
    fmt.Printf("step 3, val: %#v, i1: %#v, i2: %#v, i2_data: %#v\n", val, i1, i2, *(i2.(*int)))

    ptr1 := unsafe.Pointer(&i1)
    opt1 := (*[2]unsafe.Pointer)(ptr1)
    // i1 中的 data 的内容是一块新内存的地址 (0x1129c80), 指向的副本值仍为 100
    fmt.Printf("interface i1: %#v, %#v, %#v\n", opt1[0], opt1[1], *(*int)(unsafe.Pointer(uintptr(opt1[1]))))    

    ptr2 := unsafe.Pointer(&i2)
    opt2 := (*[2]unsafe.Pointer)(ptr2)
    // i2 中的 data 的值为 val 的地址,即 0xc0000b2008, 指向val 的指针,值被修改为了 200
    fmt.Printf("interface i2: %#v, %#v, %#v\n", opt2[0], opt2[1], *(*int)(unsafe.Pointer(uintptr(opt2[1]))))    

    i1 = 300            // 修改i1 的值, 不影响val
    fmt.Printf("step 4, val: %#v, i1: %#v, i2: %#v, i2_data: %#v\n", val, i1, i2, *(i2.(*int)))
    *(i2.(*int)) = 400  // 修改i2 的值, 会影响val
    fmt.Printf("step 5, val: %#v, i1: %#v, i2: %#v, i2_data: %#v\n", val, i1, i2, *(i2.(*int)))

    // i1 中的 data 的内容是一块新内存的地址 (0x1129c80), 指向的副本的值北修改为了 300
    fmt.Printf("interface i1: %#v, %#v, %#v\n", opt1[0], opt1[1], *(*int)(unsafe.Pointer(uintptr(opt1[1]))))
    // i2 中的 data 的值为 val 的地址,即 0xc0000b2008, 指向val 的指针,值被修改为了 400
    fmt.Printf("interface i2: %#v, %#v, %#v\n", opt2[0], opt2[1], *(*int)(unsafe.Pointer(uintptr(opt2[1]))))
}
// 输出如下:
// step 1, val: 100, type: int, addr: 0xc00001a0a8
// step 2, val: 100, i1: 100, i2: (*int)(0xc00001a0a8), i2_data: 100
// step 3, val: 200, i1: 100, i2: (*int)(0xc00001a0a8), i2_data: 200
// interface i1: (unsafe.Pointer)(0x1092640), (unsafe.Pointer)(0x1129c80), 100
// interface i1: (unsafe.Pointer)(0x108fc80), (unsafe.Pointer)(0xc00001a0a8), 200
// step 4, val: 200, i1: 300, i2: (*int)(0xc00001a0a8), i2_data: 200
// step 5, val: 400, i1: 300, i2: (*int)(0xc00001a0a8), i2_data: 400
// interface i1: (unsafe.Pointer)(0x1092640), (unsafe.Pointer)(0x10c2038), 300
// interface i1: (unsafe.Pointer)(0x108fc80), (unsafe.Pointer)(0xc00001a0a8), 400

3.2 空接口interface

空接口就是没有抽象方法的接口。 var ifc interface{} ifc 就是一个空接口类型变量,可以把任意类型的值 赋给一个 空接口 类型的变量。

空接口的底层数据结构的定义如下:

1
2
3
4
5
// runtime/runtime2.go
type eface struct {
    _type *_type            // 动态类型, 指向实际类型描述的指针
    data  unsafe.Pointer    // 原数据地址, 保存实际变量的地址
}

eface 包含了2个元素:

  • _type,指向对象的类型元数据,在编译的时候生成在可执行文件后随着可执行文件加载进入.text的 .rodata 区域内;
  • data,指针指向绑定对象的原始数据;

_type 是指向实际类型的描述信息 _type 表示类型信息,是所有类型最原始的元信息,位于src/runtime/type.go,每个类型的 _type 信息由编译器在编译时生成。 Go 语言是强类型语言,编译时对每个变量的类型信息做强校验,所以每个类型的元信息要用一个结构体描述,再者 Go 的反射也是基于类型的元信息实现的。

像类型名称,大小,对齐边界,是否为自定义类型等信息,是每个类型元数据都要记录的,所以被放到了runtime._type结构体中,作为每个类型元数据的Header。 _type结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/runtime/type.go
type _type struct {
    size       uintptr // 类型占用内存大小
    ptrdata    uintptr // 包含所有指针的内存前缀大小
    hash       uint32  // 类型 hash
    tflag      tflag   // 标记位,主要用于反射
    align      uint8   // 对齐字节信息
    fieldAlign uint8   // 当前结构字段的对齐字节数
    kind       uint8   // 基础类型枚举值
    equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较两个形参对应对象的类型是否相等
    gcdata    *byte    // GC 类型的数据
    str       nameOff  // 类型名称字符串在二进制文件段中的偏移量
    ptrToThis typeOff  // 类型元信息指针在二进制文件段中的偏移量
}

重点解释一下以下三个字段,其余的都在备注里进行了说明: 1)kind,这个字段描述的是如何解析基础类型。在 Go 语言中,基础类型是一个枚举常量,有 26 个基础类型,枚举值通过 kindMask 取出特殊标记位。 2)str 和 ptrToThis,对应的类型是 nameoff 和 typeOff。分表表示name和type针对最终输出文件所在段内的偏移量。在编译的链接步骤中,链接器将各个 .o 文件中的段合并到输出文件,会进行段合并,有的放入 .text 段,有的放入 .data 段,有的放入 .bss 段。nameoff和typeoff就是记录了对应段的偏移量。

_type 类型代表了Golang 所有的数据类型的元数据。所有数据类型都是在它的基础上,增加其它字段来扩展的,如 chan 类型:

1
2
3
4
5
6
// runtime/type.go
type chantype struct {
    typ  _type
    elem *_type
    dir  uintptr
}

即 _type 是存储了类型的元数据。简单理解,就是_type能确定数据类型。类型断言,就是用该字段信息做的判断。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

//Student结构体
type Student struct {
    name string
}

//Student方法setName
func (s *Student) setName(name string) {
    s.name = name
}

//Student方法getName
func (s *Student) getName() string {
    return s.name
}

func main() {
    //声明一个空接口变量a
    var a interface{}
    s := &Student{"Jack"}
    a = s
    fmt.Println(a)
}

把Student类型的变量s赋给a,那么变量a的结构就如下图所示:

通过 gdb 调试信息查看验证是否正确:

1
2
3
(gdb) info locals
a = {_type = 0x10c0980 <type:*+46208>, data = 0xc000014270}
s = 0xc000014270

3.2 非空接口

非空接口就是有抽象方法的接口。它的底层结构如下:

1
2
3
4
5
// runtime/runtime2.go
type iface struct {
    tab  *itab // 动态类型
    data unsafe.Pointer // 动态类型数据地址
}

非空接口类型结构同样包含两个字段:

  • data 字段的意义跟空接口是一样,指针指向绑定对象的原始数据;
  • tab,存放的是类型、方法等信息;

tab所对应的结构体是 runtime.itab,该结构体是接口类型的核心组成部分;每一个 runtime.itab 都占 32 字节,可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示: itab 数据结构:

1
2
3
4
5
6
7
type itab struct {
    inter *interfacetype // 接口自身定义的类型信息,用于定位到具体interface类型
    _type *_type         // 接口的具体类型,指向实际对象类型
    hash  uint32         // _type.hash的拷贝,是类型的哈希值,用于快速查询和判断目标类型和接口中类型是一致
    _     [4]byte        // 填充字段,保证对齐用
    fun   [1]uintptr     // 动态数组,接口方法实现列表(方法集),即函数地址列表,按字典序排序,如果数组中的内容为空表示 _type 没有实现 inter 接口
}
  • itab._type 上文已经解释过,表示类型信息。每个类型的 _type 信息由编译器在编译时生成。其中:

    • size 为该类型所占用的字节数量。
    • kind 表示类型的种类,如 bool、int、float、string、struct、interface 等。
    • str 表示类型的名字信息,它是一个 nameOff(int32) 类型,通过这个 nameOff,可以找到类型的名字字符串
    • 灰色的 extras 对于基础类型(如 bool,int, float 等)是 size 为 0 的,它为复杂的类型提供了一些额外信息。例如为 struct 类型提供 structtype,为 slice 类型提供 slicetype 等信息。
    • 灰色的 ucom 对于基础类型也是 size 为 0 的,但是对于 type Binary int 这种定义或者是其它复杂类型来说,ucom 用来存储类型的函数列表等信息。
    • 注意 extras 和 ucom 的圆头箭头,它表示 extras 和 ucom 不是指针,它们的内容位于 _type 的内存空间中。
  • itab.inter 是结构为 interfacetype 的类型元数据,它里面记录了这个接口类型的描述信息,接口要求(定义)的方法列表就记录在interfacetype.mhdr,

interfacetype 是 _type 为 interface 类型提供的另一种信息,它包括这个 interface 所申明的所有函数信息。

结构体代码如下:

1
2
3
4
5
type interfacetype struct {
    typ     _type      // 接口类型
    pkgpath name       // 包路径
    mhdr    []imethod  // 接口中的方法表
}

其中 interfacetype 结构体的 mhdr 字段涉及到 imethod类型,imethod 结构体用于描述接口类型的方法;method 结构体用于描述常规类型的方法。 其结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 接口类型的方法
type imethod struct {
    name nameOff  // 方法名称在名称表中的偏移量
    ityp typeOff  // 方法类型在类型表中的偏移量
}

// 非接口类型的方法,它是一个压缩格式的结构,每个字段的值都是一个相对偏移量
type method struct {
    name nameOff  // 方法名称在名称表中的偏移量
    mtyp typeOff  // 方法类型在类型表中的偏移量
    ifn  textOff  // 接口方法的实现函数在代码段中的偏移量
    tfn  textOff  // 普通方法的实现函数在代码段中的偏移量
}

interfacetype 结构体的 pkgpath 代表接口所在的包名,mhdr 表示的接口方法。

  • itab.fun 它是一个用于动态派发的虚函数表,存储了一组函数指针; 记录的是动态类型实现的那些接口要求的方法的地址,是从方法元数据中拷贝来的,为的是快速定位到方法。如果itab._type对应的类型没有实现这个接口,则itab.fun[0]=0 ; 当fun[0]为0时,说明_type并没有实现该接口,当有实现接口时,fun存放了第一个接口方法的地址,其它方法一次往下存放,这里就简单用空间换时间,其实方法都在_type字段中能找到,实际在这记录下,每次调用的时候就不用动态查找了。

看一个例子,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
    "fmt"
)

type Square interface {
    Area() float64
    Perimeter() float64
}

type Sdata struct {
    x, y float64
}

func (s *Sdata) Area() float64 {
    return s.x * s.y
}

func (s *Sdata) Perimeter() float64 {
    return (s.x + s.y) * 2
}

func NewSdata(x, y float64) *Sdata {
    return &Sdata{
        x: x,
        y: y,
    }
}

func main() {
    var s Square
    Object := NewSdata(1, 2)
    s = Object
    fmt.Println(s)
}

通过 gdb 调试信息,看下变量s赋值前的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-----------  赋值前 ---------------
(gdb) p s
$2 = {tab = 0x0, data = 0x0}

-----------  赋值后 ---------------
(gdb) p s
$1 = {tab = 0x10dd9b8 <go:itab.*main.Sdata,main.Square>, data = 0xc0000b4010}
(gdb) ptype s
type = struct runtime.iface {
    runtime.itab *tab;
    void *data;
}
(gdb) p Object
$2 = (main.Sdata *) 0xc0000b4010

从调试结果来看,s 为非空接口类型变量,赋值前对应结构和数据为:{tab = 0x0, data = 0x0};赋值后对应的数据和结构:{tab = 0x10dd9b8 <go:itab.*main.Sdata,main.Square>, data = 0xc0000b4010} 用图表示关系如下:

四、接口变量的类型转换

4.1 指针类型转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

type People interface {
    GetName() string
}

//go:noinline
func (s *Student) GetName() string {
    return s.name
}

type Student struct {
    name string
    age  int
}

func main() {
    var p People
    var s *Student = &Student{
        name: "XJX",
        age:  15,
    }
    p = s
    p.GetName()
}

利用go tool compile命令将上述代码变成汇编代码:

  • 初始化Student对象:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
0x0030 00048 (main.go:32)       LEAQ    type."".Student(SB), AX                    ;AX = &type."".Student
0x0037 00055 (main.go:32)       MOVQ    AX, (SP)                                ;SP = &type."".Student
0x003b 00059 (main.go:32)       PCDATA  $1, $0
0x003b 00059 (main.go:32)       NOP
0x0040 00064 (main.go:32)       CALL    runtime.newobject(SB)                    ;SP + 8 = &Student{}
0x0045 00069 (main.go:32)       MOVQ    8(SP), DI                               ;DI = &Student{}
0x004a 00074 (main.go:32)       MOVQ    DI, ""..autotmp_3+32(SP)                ;autotmp_3(32SP) = &Student{}
0x004f 00079 (main.go:33)       MOVQ    $3, 8(DI)                               ;StringHeader(DI.Name).Len = 3
0x0057 00087 (main.go:33)       PCDATA  $0, $-2
0x0057 00087 (main.go:33)       CMPL    runtime.writeBarrier(SB), $0
0x005e 00094 (main.go:33)       NOP
0x0060 00096 (main.go:33)       JEQ     100
0x0062 00098 (main.go:33)       JMP     191
0x0064 00100 (main.go:33)       LEAQ    go.string."XJX"(SB), AX                 ;AX = &"XJX"
0x006b 00107 (main.go:33)       MOVQ    AX, (DI)                                ;StringHeader(DI.Name).Data = &"XJX"
0x006e 00110 (main.go:33)       JMP     112
0x0070 00112 (main.go:32)       PCDATA  $0, $-1
0x0070 00112 (main.go:32)       MOVQ    ""..autotmp_3+32(SP), AX                ;AX = &Student{}
0x0075 00117 (main.go:32)       TESTB   AL, (AX)
0x0077 00119 (main.go:34)       MOVQ    $15, 16(AX)                             ;DI.age = 15
0x007f 00127 (main.go:32)       MOVQ    ""..autotmp_3+32(SP), AX                 ;AX = &Student{}
  1. 先将 &type."".Student 放在 (SP) 栈顶。
  2. 然后调用 runtime.newobject() 在堆中生成 Student 对象并且返回地址。(SP) 栈顶的值即是 newobject() 方法的入参。
  3. 后续将 name 和 age 信息补充完整。因为 name 为字符串,字符串的结构为 StringHeader,StringHeader有 Len 和 Data两个属性值分别为 3 和 XJX字符串存放地址; age 则为 int 基础类型,直接赋值即可。 用图表示如下
  • 把Student对象转化为Person interface指针: 继续看代码:
1
2
3
0x008e 00142 (main.go:36)       LEAQ    go.itab.*"".Student,"".People(SB), CX;AX = *itab(go.itab.*"".Student,"".People)
0x0095 00149 (main.go:36)       MOVQ    CX, "".p+48(SP)                        ;p(48SP) = *itab(go.itab.*"".Student,"".People)
0x009a 00154 (main.go:36)       MOVQ    AX, "".p+56(SP)                        ;p(56SP) = &Student{}
  1. 经过上面几行汇编代码,成功的构造出了 itab 结构体以及iface,如图:
  • 调用interface方法
1
2
3
4
5
6
0x009f 00159 (main.go:37)       MOVQ    "".p+48(SP), AX                    ; AX = *itab(go.itab.*"".Student,"".People)
0x00a4 00164 (main.go:37)       TESTB   AL, (AX)
0x00a6 00166 (main.go:37)       MOVQ    24(AX), AX                        ; AX = *(GetName)
0x00aa 00170 (main.go:37)       MOVQ    "".p+56(SP), CX                    ; CX = &Student{}
0x00af 00175 (main.go:37)       MOVQ    CX, (SP)                        ; 移动CX到栈顶
0x00b3 00179 (main.go:37)       CALL    AX                                ;call GetName func
  1. 取出 itab(go.itab."".Student,"".People)地址存放到AX寄存器,然后移动 +24字节,获取第一个函数的地址存入AX,调用AX即可。

4.2 结构体类型转换

来看下结构体类型的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type People interface {
    GetName() string
}

//go:noinline
func (s Student) GetName() string {
    return s.name
}

type Student struct {
    name string
    age  int
}

func main() {
    var p People
    var s Student = Student{
        name: "XJX",
        age:  15,
    }
    p = s
    p.GetName()
}

基本跟指针类型转换差不多,但有几点差别:

  • 编译器发现变量只是临时变量时,没有调用 runtime.newobject(),仅仅是将它的每个基本类型的字段生成好放在内存中。然后如果涉及到逃逸,则使用函数runtime.convTstring函数将数据复制拷贝到堆区一份,runtime.convTstring代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// iface.go
// 在堆上分配了stringStruct并赋值,返回x为对象地址。
func convTstring(val string) (x unsafe.Pointer) {
    if val == "" {
        x = unsafe.Pointer(&zeroVal[0])
    } else {
        x = mallocgc(unsafe.Sizeof(val), stringType, true)
        *(*string)(x) = val
    }
    return
}
  • 初始化结构体后会进入类型转换的阶段,编译器会将 go.itab.""..Student,""..People 的地址和指向 Student 结构体的指针作为参数一并传入 runtime.convT2I函数:
1
2
3
4
5
6
7
8
9
// runtime.convT2I会返回一个runtime.iface,其中包含runtime.itab指针和 Student 变量。
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    x := mallocgc(t.size, t, true)
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}
  • 在汇编调试过程中,发现一个现象,如果使用如下结构体方式赋值,编译器可能会进行优化,变量基本通过临时变量存储在栈中,也并不会存在类型转换调用 runtime.convT2I函数,而跟指针类型类型直接在栈内构造完成,代码如下:
1
2
3
4
5
var p People = Student{
    name: "XJX",
    age:  15,
}
p.GetName()

五、接口变量的类型断言

5.1 类型断言简介

在 Golang 中 interface变量 可以存放任何实现了该接口类型,所以 Golang 提供了类型断言来判断某一时刻接口变量中所包含的实际类型。类型断言 是 Golang 中应用在接口(interface)值上的一个特性。

类型断言 的目标类型可以是某种具体类型,也可以是某种非空接口类型。

接口类型断言的语法格式为:

1
2
3
val := interfaceVar.(T)       // 此方式断言失败会 panic 报相关错误信息
// or
val, err := interfaceVar.(T)  // 此方式断言失败不会 panic,val 为0、err返回false
  • interfaceVar 是一个接口类型变量
  • T 是某一种要判断的具体类型名称
  • 如果断言成功,将返回接口转换为实际类型(T)的变量值(val)、err 值为true,断言失败时 err 值为false

所以一般接口断言常用以下写法:

1
2
3
4
if v, err := interfaceVar.(T); err { // T是一种类型
    possess(v)  // 处理v
    return
}

如果转换合法,则 v 为 interfaceVar 转换为类型T的值,err 为 ture,反之 err 为 false。

5.2 空接口变量的类型断言

空接口(interface{})类型变量的类型断言有两类,分别为 对具体类型的断言 和 对其它非空接口类型 的断言。

先从例子出发,看一段空接口断言的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type Student struct {
    name string
}

type People interface {
    GetName() string
}

func (s *Student) GetName() string {
    return s.name
}

func main() {
    var s interface{} = &Student{name: "XJX"} // s 是空接口类型变量,其动态类型 为 *Student 类型
    v, ok := s.(int)    // 空接口类型变量 s 对 具体类型(int) 进行类型断言
    if ok {
        fmt.Printf("%v\n", v)
    }
    switch s.(type) {
    case People: // 空接口类型变量 s 对 其它非空接口类型(People) 进行类型断言
    }
}

通过反编译查看源码 空接口变量 s 对 int 类型的断言:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
go tool compile -N -l -S main.go

......
0x0070 00112 (main.go:17)       LEAQ    type.*"".Student(SB), CX   ; 把eface _type的地址放入CX
0x0077 00119 (main.go:17)       MOVQ    CX, "".s+120(SP)           ; 把CX存放值赋值给s+120(SP)
0x007c 00124 (main.go:17)       MOVQ    AX, "".s+128(SP)           ; 把AX存放值赋值给s+128(SP)
0x0084 00132 (main.go:18)       MOVQ    "".s+120(SP), AX           ; 把s+120(SP)存放寄存器AX,即eface _type地址
0x0089 00137 (main.go:18)       MOVQ    "".s+128(SP), CX           ; 把s+128(SP)存放寄存器CX,即eface data地址
0x0091 00145 (main.go:18)       LEAQ    type.int(SB), DX           ; 把int的类型type的地址放到 DX
0x0098 00152 (main.go:18)       CMPQ    DX, AX                     ; 直接比较 AX DX的地址
......

类型元数据都存储在.rodata区域内,要判断接口变量的动态类型是否为指定类型,只需获取空接口的_type的地址位置和需要断言的类型的_type位置进行比较,如果相同则代表类型一致。

下面看看空接口变量 s 对于接口类型(People)断言的汇编:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
0x01b2 00434 (main.go:30)       MOVL    AX, ""..autotmp_14+68(SP)
0x01b6 00438 (main.go:31)       MOVQ    ""..autotmp_12+168(SP), AX
0x01be 00446 (main.go:31)       MOVQ    ""..autotmp_12+176(SP), CX
0x01c6 00454 (main.go:31)       LEAQ    type."".People(SB), DX    ;获取People的类型元素存入DX
0x01cd 00461 (main.go:31)       MOVQ    DX, (SP)                  ;放入栈顶
0x01d1 00465 (main.go:31)       MOVQ    AX, 8(SP)                  ;放入eface
0x01d6 00470 (main.go:31)       MOVQ    CX, 16(SP)
0x01db 00475 (main.go:31)       PCDATA  $1, $0
0x01db 00475 (main.go:31)       NOP
0x01e0 00480 (main.go:31)       CALL    runtime.assertE2I2(SB)    ;判断发起调用
0x01e5 00485 (main.go:31)       MOVBLZX 40(SP), AX                ;bool值
0x01ea 00490 (main.go:31)       MOVB    AL, ""..autotmp_13+67(SP)

从汇编代码可以看出,空接口对于接口的断言,主要是调用 assertE2I2函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// src/runtime//iface.go
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
    //获取eface的类型元数据
    t := e._type
    if t == nil {
        return
    }
    tab := getitab(inter, t, true)
    if tab == nil {
        return
    }
    r.tab = tab
    r.data = e.data
    b = true
    return
}

原理也很简单,从空接口的eface的_type里取出类型元数据,再根据传入的interfacetype 去判断是否有对应的接口实现,成功则断言成功,否则返回nil和false。

5.3 非空接口变量的类型断言

非空接口类型变量的类型断言同样有两类,分别为 对具体类型的断言 和 对其它非空接口类型 的断言。

非空接口还是直接从例子开始看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type People interface {
    GetName() string
}

type Student struct {
    name string
}

func (s *Student) GetName() string {
    return s.name
}

func main() {
    var p People = &Student{name: "XJX"} // p 是 People接口类型变量,其动态类型 为 *Student 类型
    v, ok := p.(People)  // 非空接口类型(People)变量 p 对 非空接口类型(People)进行类型断言
    if ok {
        fmt.Printf("%v\n", v)
    }
    v, ok = p.(*Student)  // 非空接口类型(People)变量 p 对 具体类型(*Student) 进行类型断言
    if ok {
        fmt.Printf("%v\n", v)
    }
}

通过反编译查看源码 接口变量 p 对 具体类型(*Student)的类型断言:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
go tool compile -N -l -S main.go

....... 
0x0207 00519 (main.go:30)    MOVQ    $0, ""..autotmp_3+88(SP)
0x0210 00528 (main.go:30)    MOVQ    "".p+144(SP), AX                           ;放入具体的AX
0x0218 00536 (main.go:30)    LEAQ    go.itab.*"".Student,"".People(SB), CX      ;获取具体类型的itab的地址
0x021f 00543 (main.go:30)    NOP
0x0220 00544 (main.go:30)    CMPQ    "".p+136(SP), CX                            ;比较两个itab的地址进行类型断言
0x0228 00552 (main.go:30)    JEQ    559
0x022a 00554 (main.go:30)    JMP    871
....... 

具体类型的断言和空接口的例子比较类似编译器也通过汇编代码优化进行了实现,没有去调用runtime函数。

简单补充下 p+144(SP) 放入的是p的具体值,p+136(SP)放入的是p的itab,这个在前面的汇编函数已经实现,因为代码省略的原因特殊说明下。

下面看看非空接口变量 p 对于接口类型(People)断言的汇编:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.......
0x00a1 00161 (main.go:26)    MOVUPS    X0, ""..autotmp_4+168(SP)
0x00a9 00169 (main.go:26)    MOVQ    "".p+136(SP), AX
0x00b1 00177 (main.go:26)    MOVQ    "".p+144(SP), CX
0x00b9 00185 (main.go:26)    LEAQ    type."".People(SB), DX       ;获取type People的类型元数据地址到 DX
0x00c0 00192 (main.go:26)    MOVQ    DX, (SP)                     ;DX存入栈底
0x00c4 00196 (main.go:26)    MOVQ    AX, 8(SP)                    ;iface存入8(SP)
0x00c9 00201 (main.go:26)    MOVQ    CX, 16(SP)
0x00ce 00206 (main.go:26)    PCDATA    $1, $1
0x00ce 00206 (main.go:26)    CALL    runtime.assertI2I2(SB)       ;调用方法进行类型断言
0x00d3 00211 (main.go:26)    MOVBLZX    40(SP), AX                   ;返回参数 ok bool值
0x00d8 00216 (main.go:26)    MOVQ    24(SP), CX                   ;新的iface
0x00dd 00221 (main.go:26)    MOVQ    32(SP), DX                   ;新的iface
....... 

从汇编代码可以看出,非空接口对于接口的断言,主要是调用 assertI2I2函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/runtime//iface.go
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
    tab := i.tab //获取变量i的iface.tab
    if tab == nil {
        return
    }
    if tab.inter != inter { //如果i的face.tab跟inter不一致
        tab = getitab(inter, tab._type, true) //使用inter类型与tab._type具体类型查询itab
        if tab == nil { //如果查询不到,则表示断言失败
            return
        }
    }
    //断言成功 赋值
    r.tab = tab
    r.data = i.data
    b = true
    return
}

通过这样的方式就实现了接口类型断言。

5.4 接口变量的类型断言总结

用一张图来总结下类型断言: 空接口类型变量的断言 编译器会根据要判断的是类推(T)使用:

  • 直接使用接口变量的 _type 元数据 与断言类型(T)的元数据比较来断言一个 具体类型
  • 调用 runtime.assertE2I2 判断接口变量的 _type 元数据是否为 断言的接口类型(T);

非空接口类型变量的断言 和空接口也类似,也会根据要断言的是 具体类型 还是 接口类型 去判断是直接比较 还是 调用runtime.assertI2I2比较。

runtime.assertE2I2runtime.assertI2I2 函数底层都调用了 getitab 函数去全局查找符合的 itab 来完成类型的判断。

六、结构体内嵌接口

6.1 结构体内嵌接口介绍

在 Golang 中,结构体 可以内嵌 结构体、接口 可以内嵌 接口,这两类内嵌比较常见,除此之外,结构体 也可内嵌 匿名接口。

当需要重写一个 已经实现了某个接口的结构体 的部分方法,而其它方法保持不变 的时候,就需要用到 结构体内嵌接口 这种用法。

下面以计算器为例子进行说明,定义一个计算器接口,拥有 加法乘法 两个方法,并定义一个结构体实现这个接口,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type Icalculator interface {
    sum(a, b int) int      // 加法
    multiply(a, b int) int // 乘法
}

// 定义 MyCalc 结构体,并实现 Icalculator 接口中的方法
type MyCalc struct{}

func (obj MyCalc) sum(a, b int) int {
    return a + b
}

func (obj MyCalc) multiply(a, b int) int {
    return a * b
}

func main() {
    myCalc := MyCalc{}

    a, b := 5, 3
    fmt.Printf("sum: %d, multiply: %d\n", myCalc.sum(a, b), myCalc.multiply(a, b))
    // 输出:sum: 8, multiply: 15
}

至此,有了一个计算器结构体,并可以正常使用。

假设现在有一个需求,需要改写MyCalc的sum方法,将加法改成减法,并且乘法方法保持不变,这时候可以怎么做呢?

这时,可以新增一个结构体,重新实现接口中的所有方法来达到这个目标,但是如果这个接口中有很多方法,而只是想重写sum方法而已,全部重新实现一遍所有的方法就太过重复了,其它方法是重复的代码。

这时候就需要用到 结构体 内嵌 匿名接口 的用法了,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import "fmt"

type Icalculator interface {
    sum(a, b int) int      // 加法
    multiply(a, b int) int // 乘法
}

// 定义 MyCalc 结构体,并实现 Icalculator 接口中的方法
type MyCalc struct{}

func (obj MyCalc) sum(a, b int) int {
    return a + b
}

func (obj MyCalc) multiply(a, b int) int {
    return a * b
}

// 新增定义一个 CustomCalc 结构体,并匿名内嵌 Icalculator 接口
type CustomCalc struct {
    Icalculator
}

// 改写sum方法
func (obj CustomCalc) sum(a, b int) int {
    return a - b
}

func NewCustomCalc(icalc Icalculator) Icalculator {
    return &CustomCalc{Icalculator: icalc}

}
func main() {
    myCalc := MyCalc{}
    // 将myCalc传入CustomCalc结构体
    customCalc := CustomCalc{myCalc}

    a, b := 5, 3
    fmt.Printf("sum: %d, multiply: %d\n", customCalc.sum(a, b), customCalc.multiply(a, b))
    // 输出:sum: 2, multiply: 15

    icalc := NewCustomCalc(myCalc)
    fmt.Printf("sum: %d, multiply: %d\n", icalc.sum(a, b), icalc.multiply(a, b))
}

通过新增一个CustomCalc结构体,并使用 myCalc(MyCalc类型)对象初始化 customCalc对象,也可传入 Icalculator类型对象进行初始化,CustomCalc结构体定义一个sum方法,这个sum方法会覆盖myCalc的同名方法,从而实现了sum方法的重写。

结构体内嵌接口的作用总结:

  • 不依赖具体实现:即接口为 A,结构体 B1、B2 实现了接口 A,结构体 C 内嵌了 A,那么 C.A 可以通过 A、B1 或 B2 类型的对象来实例化;
  • 对接口类型进行重写:当 C.A 通过 B1 实例化后,C 和 B1 的关系,可以转变为结构体 C 内嵌结构体B1,那么 C 可以直接使用B1 中的所有方法,当然 C 也可以对 B1 中的方法进行 部分 或 全部 重写,这里官方文档这样解释 “Interface and we can override a specific method without having to define all the others.”

一个结构体包含了一个接口,那么此结构体自然就是这个接口的一个实现,即便这个结构体没有实现任何方法。 也就是说,把 interface 作为 struct 的一个匿名成员,就可以假设 struct 就是此成员 interface 的一个实现,而不管 struct 是否已经实现 interface 所定义的方法。此时可以把 结构体变量 或 结构体指针变量 赋值给 成员 interface 变量。

结构体对象 和 赋值过的 interface 变量 只能调用实现(定义了)的接口方法,调用未定义的接口方法将导致程序 panic 退出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

type man interface {
    Eat(args ...any)
    Run()
}

type dog struct {
    man
}

func (d dog) Run() {
}

func main() {
    d := dog{}
    d.Run() // 可以调用实现了的方法
    // d.Eat(1) // 不可以调用未实现的方法

    var m man = d // 可以赋值,说明 d 实现了 man 接口
    m.Run() // 可以调用实现了的方法
    // m.Eat(1) // 不可以调用未实现的方法

    var m1 man = &d // 可以赋值,说明 d 实现了 man 接口
    m1.Run() // 可以调用实现了的方法
    // m.Eat(1) // 不可以调用未实现的方法
}

代码可以通过编译链接生成可执行文件,但是执行的时候 panic 异常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
192:SourceCodeTest mac$ go build t.go 
192:SourceCodeTest mac$ ls
go.mod t      t.go   
192:SourceCodeTest mac$ ./t 
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x105769c]

goroutine 1 [running]:
main.main()
    /Users/mac/MubeiSpaces/NoteOnLearning/SourceCodeTest/t.go:14 +0x3c

golang接口的这种隐式的实现特性,会导致某个对象无意间就实现了某个接口

对于一些底层接口却需要保持其封闭性,为了达到这个目的,通常的做法是,在接口中有特殊含义的方法,比如runtime.Error接口,注释就说明了意图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// The Error interface identifies a run time error.
type Error interface {
    error

    // RuntimeError is a no-op function but
    // serves to distinguish types that are run time
    // errors from ordinary errors: a type is a
    // run time error if it has a RuntimeError method.
    RuntimeError()
}

或者定义一个无法导出的方法,这样在包外面就无法被实现了,比如testing.TB接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// TB is the interface common to T, B, and F.
type TB interface {
    Cleanup(func())
    Error(args ...any)
    Errorf(format string, args ...any)
    Fail()
    FailNow()
    Failed() bool
    Fatal(args ...any)
    Fatalf(format string, args ...any)
    Helper()
    Log(args ...any)
    Logf(format string, args ...any)
    Name() string
    Setenv(key, value string)
    Skip(args ...any)
    SkipNow()
    Skipf(format string, args ...any)
    Skipped() bool
    TempDir() string

    // A private method to prevent users implementing the
    // interface and so future additions to it will not
    // violate Go 1 compatibility.
    private()
}