interface引发的事件真相

interface引发的事件真相

(PS:专栏所有的代码都是基于go version go1.8 darwin/amd64)

流动的水没有形状,漂流的风找不到踪迹,一切代码都了然于心,我们在写代码的时候,总是有一种思维定式陪伴左右,在对事物做判断的时候,往往这种思维定式会往正向或反向做推动作用,在开发的过程中如果不小心忽略,往往就是埋下了陷阱,以下代码是大多数新手会遇到的坑,

package main

import (
	"fmt"
)

type People interface {
	Name() string
}
type Student struct{ name string }

func (stu *Student) Name() string {
	return stu.name
}

func getPeople() People {
	var stu *Student
	return stu
}

func main() {
	if getPeople() == nil {
		fmt.Println("AAAAA")
	} else {
		fmt.Println("BBBBB")
	}
}

上面的代码输出什么那?有些人会认为打印AAAAA,因为他们会认为getPeople方法里面stu是nil 所以返回的就是nil,这样想就大错特错,因为虽然返回的stu是nil 但是函数返回时People接口的结构的本身并不是nil,在我们不了解interface内部结构之前请往下看。

为什么我会选择去写一个关于interface的文章那,我认为他在go语言里面有这非常重要的地位,仅次于goroutine和channel的地位,我在未接触go之前一直从事于c#的开发,接口对我来说就是不同组件之间的契约,对这个契约强制你必须去继承接口,而go语言的设计就非常轻巧,只要实现了接口所要求的所有函数即可,go中的接口分为两种一种是空的接口类似这样:

var in interface{}

例外一种是非空的接口即在接口内部声明了一些方法:

type People interface {
	Name() string
}

接下来我就根据上面的例子来对比一下空接口和非空接口内部结构

type eface struct {          //空接口
	_type *_type         //类型信息
	data  unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct {          //带有方法的接口
	tab  *itab           //存储type信息还有结构实现方法的集合
	data unsafe.Pointer  //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}

eface包含一个类型信息,可以为reflect提供帮助

type _type struct {
	size       uintptr  //类型大小
	ptrdata    uintptr  //前缀持有所有指针的内存大小
	hash       uint32   //数据hash值
	tflag      tflag    
	align      uint8    //对齐
	fieldalign uint8    //嵌入结构体时的对齐
	kind       uint8    //kind 有些枚举值kind等于0是无效的
	alg        *typeAlg //函数指针数组,类型实现的所有方法
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}

iface比eface 中间多了一层itab结构

type itab struct {
	inter  *interfacetype  //接口类型
	_type  *_type          //结构类型
	link   *itab           
	bad    int32
	inhash int32      
	fun    [1]uintptr      //可变大小 方法集合
}

itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil,所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口,接口的具体细节你可以阅读Go Data Structures: Interfaces

接下来是第二个例子:

package main

import (
	"fmt"
)

type People interface {
	Speak(string) string
}

type Stduent struct{}

func (stu *Stduent) Speak(think string) (talk string) {
	if think == "bitch" {
		talk = "You are a good boy"
	} else {
		talk = "hi"
	}
	return
}

func main() {
	var peo People = Stduent{}
	think := "bitch"
	fmt.Println(peo.Speak(think))
}

上面的代码是不能编译过去的,会提示没有实现该接口,只要我们把var peo People = Stduent{}修改为var peo People = &Stduent{}就可以了,为什么会有这种限制,

这是因为接口定义不规定实现者是否应该使用指针接收还是值接收实现接口。当使用接口时,不能保证底层类型是值还是指针。我们上面的例子中,我们定义了指针接受方法,修改为值接受方法:

func (stu Stduent) Speak(think string) (talk string) {
	if think == "bitch" {
		talk = "You are a good boy"
	} else {
		talk = "hi"
	}
	return
}

我们再次运行打印:

You are a good boy

通过上面测试我们得出一个结论使用值传递方法,接口赋值使用var peo People = Stduent{}或者var peo People = &Stduent{},如果使用指针作为参数传递,则只能使用var peo People = &Stduent{},正是由于interface的灵活性,可以使用golang实现多态的特性,所以我们更要对interface多做深入了解。(本文未来可能会做一些细微的调整)

编辑于 2017-06-01

文章被以下专栏收录