Go总结:接口与方法集

目录
  1. 1. 接口的认识
  2. 2. 接口方法集
  3. 3. 敲黑板,重点来了

Go学习第四篇文章已经学习了类型的方法集,分值接收者和指针接收者,而且值和指针变量都可以自由调用这些方法。但接口的变量却不能随意调用实现者的方法集,这里有文章。

接口的认识

Go语言中接口(interface)非常重要,他被用来约定一组行为,凡是具备这一组行为的类型,都可以看做是该接口的派生类型。利用这种特性,我们就能抽象出一类行为,将来功能的实现可以完全取决于具体的调用者。这种具备不同行为能力的特性叫多态。这也是Go语言中为数不多的典型的面向对象特性。他简单易懂功能强大,为Go的设计理念点赞。

也就是说接口只定标准,不管具体的实现。接口有下面一些特点:

  • 不能有字段
  • 只声明,不实现方法
  • 可以嵌入(mixin)其它接口类型

下面看一个例子

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 Food interface {
canEat() bool
}

type Apple struct {
price int
color int
}

func (a Apple) canEat() bool {
if a.color == 1 {
fmt.Println("This apple is eatable.")
} else {
fmt.Println("This apple inedible.")
}
return true
}

func InterfaceTest() {
var fruit Food
fruit = Apple{price: 10, color: 2}
fruit.canEat()

fmt.Println(unsafe.Sizeof(fruit))
fmt.Printf("Addr: %p, Values: %v, Type: %T\n", &fruit, fruit, fruit)

fmt.Println("-------")

var fruit2 Food
fruit2 = &Apple{price: 20, color: 1}
fruit2.canEat()

fmt.Println(unsafe.Sizeof(fruit2))
fmt.Printf("Addr: %p, Values: %v, Type: %T\n", fruit2, fruit2, fruit2)
}

运行结果如下:

1
2
3
4
5
6
7
This apple inedible.
16
Addr: 0xc00002e1f0, Values: {10 2}, Type: types.Apple
-------
This apple is eatable.
16
Addr: 0xc00000a0b0, Values: &{20 1}, Type: *types.Apple

分析:

  • interface变量居然既可以用值来赋值,也可以用指针来赋值。
  • interface变量占用2个字长,其实是2个指针。

再来个测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func InterfaceTest2() {
apple := &Apple{price: 10, color: 2}
fmt.Println(unsafe.Sizeof(apple))
fmt.Printf("Addr: %p, Values: %v, Type: %T\n", apple, apple, apple)

var fruit Food
fruit = *apple // 值
fmt.Println(unsafe.Sizeof(fruit))
fmt.Printf("Addr: %p, Values: %v, Type: %T\n", &fruit, fruit, fruit)

var fruitP Food
fruitP = apple // 引用
fmt.Println(unsafe.Sizeof(fruitP))
fmt.Printf("Addr: %p, Values: %v, Type: %T\n", fruitP, fruitP, fruitP)
}

结果如下

1
2
3
4
5
6
8
Addr: 0xc00000a090, Values: &{10 2}, Type: *types.Apple
16
Addr: 0xc00002e1f0, Values: {10 2}, Type: types.Apple
16
Addr: 0xc00000a090, Values: &{10 2}, Type: *types.Apple

大家看到没有,看看7行和12行,接口的变量既可以是值类型,也可以是引用;这也太灵活了吧。因为一个接口变量存放的就是两个指针而已,指向什么地方都可以;下面先给出两种情况的内存结构示意图:

两种情况几乎一模一样,只是标红的地方有些差异。我们用unsafe代码来猜测一下是不是这样的内存结构。

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
func getPointerValue(p uintptr) int  {
return *(*int)(unsafe.Pointer(p))
}

func Test222() {
theApple := Apple{price: 10, color: 2}
var food, foodP Food
food = theApple
foodP = &theApple

fmt.Printf("Addr: %p, Values: %v, Type: %T\n", &food, food, food)
fmt.Printf("Addr: %p, Values: %v, Type: %T\n", &foodP, foodP, foodP)

// food 对应 iTable地址
appleDefineAddr := *(*int)(unsafe.Pointer(&food))
println(appleDefineAddr)
println(getPointerValue(uintptr(appleDefineAddr)))
println("++++++++")
// foodP 对应 iTable地址
appleDefineAddrP := *(*int)(unsafe.Pointer(&foodP))
println(appleDefineAddrP)
println(getPointerValue(uintptr(appleDefineAddrP)))
//appleDefineAddrP2 := getPointerValue(uintptr(appleDefineAddrP))
//println(appleDefineAddrP2)
//println(getPointerValue(uintptr(appleDefineAddrP2)))

// 接口值变量
var valAddr = unsafe.Pointer(uintptr(unsafe.Pointer(&food)) + uintptr(8))
println(*(*int)(unsafe.Pointer(uintptr(*(*int)(valAddr)))))
println(*(*int)(unsafe.Pointer(uintptr(*(*int)(valAddr)) + uintptr(8))))
// 接口指针变量
var valAddrP = unsafe.Pointer(uintptr(unsafe.Pointer(&foodP)) + uintptr(8))
println(*(*int)(unsafe.Pointer(uintptr(*(*int)(valAddrP)))))
println(*(*int)(unsafe.Pointer(uintptr(*(*int)(valAddrP)) + uintptr(8))))
}

输出结构如下:

1
2
3
4
5
6
7
8
9
10
11
Addr: 0xc0001041e0, Values: {10 2}, Type: method.Apple
Addr: 0xc0001041f0, Values: &{10 2}, Type: *method.Apple
7802336
7654432
++++++++
7802272
7654432
10
2
10
2

结果中iTable对应的内存值有点没明白。Apple变量值对应的值倒是完全符合预期。

接口方法集

方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。

类型有一个与之相关的方法集(method set),这决定了它是否实现某个接口

  • 类型T方法集包含所有 receiver T 方法
  • 类型*T方法集包含所有 receiver T+*T 方法
  • 匿名嵌入ST方法集包含所有receiver S方法
  • 匿名嵌入*ST方法集包含所有receiver S+*S方法
  • 匿名嵌入S*S*T方法集包含所有receiver S+*S方法

下面再看一个接口方法集的例子:

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
type Food interface {
setColor(int)
setPrice(int)
}

type Apple struct {
price int
color int
}

func (a Apple) setColor(cc int) {
a.color = cc
}
func (a *Apple) setPrice(pp int) {
a.price = pp
}

func InterfaceTest3() {
var apple1 Food
apple1 = Apple {price: 15, color: 3} // 这里提示错误
apple1.setColor(1)
apple1.setPrice(16)

var apple2 Food
apple2 = &Apple {price: 15, color: 3}
apple2.setColor(1)
apple2.setPrice(16)
}

上面的例子,有个地方编译出现错误,错误信息如下:

1
2
3
# 错误信息
cannot use Apple literal (type Apple) as type Food in assignment:
Apple does not implement Food (setPrice method has pointer receiver)

提示错误是因为:

  • 第20行Apple{}是值类型,他只包含值接收者方法集,即实现了setColor,没有实现setPrice,不符合Food规范
  • 第25行&Apple{}是指针类型,他包含值和指针接收者方法集,所以setColor和setPrice都实现了,符合Food规范

为什么接口方法集会做这样的约定呢?前面我们不是看到了,值和指针变量不是可以自动做转换然后顺利调用值接收者和指针接收者方法吗?这里为什么不行了呢?

敲黑板,重点来了

网上我看很多人的博客也说不清楚这个问题;我想原因可能是这样的,看下面的例子:

1
2
3
4
5
6
7
8
9
type TheAge int
func (mi *TheAge)ShowAge() {
println(*mi)
}
func MTest1() {
var T1 TheAge = 100
T1.ShowAge()
TheAge(99).ShowAge() // 报错
}

错误信息如下,意思是无法推断出TheAge(99)的地址,因为他是一个常量,只存在于CPU寄存器中,无法取地址;进而无法调用指针接收者方法ShowAge。

1
2
cannot call pointer method on TheAge(99)
cannot take the address of TheAge(99)

同样的道理,下面的代码会报错,是因为TheAge(99)其实是一个字面量,无法获取其内存地址,所以非法。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Animal interface {
ShowAge()
}

type TheAge int
func (mi *TheAge)ShowAge() {
println(*mi)
}

func MTest2() {
var A1 Animal = TheAge(99)
A1.ShowAge()
}

因此接口的T变量只包含T接收者方法集;*T变量包含*T、T接收者的方法集。