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)
}

运行结果如下:

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)
}

结果如下

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行,接口的变量既可以是值类型,也可以是引用;这也太灵活了吧。因为一个接口变量存放的就是两个指针而已,指向什么地方都可以;下面先给出两种情况的内存结构示意图:

image-20201230002458394

两种情况几乎一模一样,只是标红的地方有些差异。我们用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))))
}

输出结构如下:

Addr: 0xc0001041e0, Values: {10 2}, Type: method.Apple
Addr: 0xc0001041f0, Values: &{10 2}, Type: *method.Apple
7802336
7654432
++++++++
7802272
7654432
10
2
10
2

Apple变量值对应的值倒是完全符合预期。但是结果中iTable对应的内存值有点变化,为啥呢?重点记住下面两句话:

注意:

  • 接口有个重要特征,将对象赋值给接口变量时,会复制该对象。
  • 避免复制对象,就取对象的地址(指针)赋值给接口。此时也会创建一个接口指针。

接口在语言核心层,其实也是一个结构体:

1
2
3
4
type iface struct{ 
   tab *itab
   data unsafe.Pointer
}

接口方法集

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

类型有一个与之相关的方法集(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)
}

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

# 错误信息,
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规范

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

敲黑板,重点来了

2020年12月的理解

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

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。

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接收者的方法集。

2023年4月补充

对象赋值给接口时,需要Copy一个新对象;而语言特地让Copy后的对象不可取地址,否则指针接收者方法如果修改对象的值,实际将改的是Copy后对象的值,容易造成非预期结果。结构体允许定义值接收者方法,调用时其实是有一次对象Copy;如果接口也支持值接收者方法,在调用时会再来一次值的Copy,问题更复杂了。更何况还有我上面说的不可取地址的问题,所以语言干脆不支持这种写法。

为啥一定要Copy对象呢?因为Go有个理念,一切传值都是值拷贝,对象传给参数为interface{}的函数时,也只能是对象值拷贝。

经典现象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

type TestStruct struct{}

func NilOrNot(v interface{}) bool {
	return v == nil
}

func main() {
	var s *TestStruct
	fmt.Println(s == nil)      // #=> true
	fmt.Println(NilOrNot(s))   // #=> false
}

$ go run main.go
true
false

出现上述现象的原因是 —— 调用 NilOrNot 函数时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,*TestStruct 类型会转换成 interface{} 类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息 TestStruct,所以转换后的变量与 nil 不相等。

这句fmt.Println(NilOrNot(nil))才会返回 true。所以根本问题是:不要偷换概念。

参考阅读:

https://www.zhihu.com/people/chen-qiang-song/posts

https://zhuanlan.zhihu.com/p/427838620

(完)