一. 匿名字段
go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
- 同名字段的情况
- 所以自定义类型和内置类型都可以作为匿名字段使用
- 指针类型匿名字段
二.接口
接口定义了一个对象的行为规范,但是定义规范不实现,由具体的对象实现规范的细节。
2.1 接口类型
在Go语言中接口(interface)是一种类型,一种抽象类型。
interface是一组method的集合,接口做的事情就像定义一个协议,不关心属性(数据),只关心行为(方法)。
2.2 为什么使用接口
查看下面的图片,只是由于类型不同,就需要定义两个逻辑一样的函数。如果后面出现了其它动物,也会需要定义函数。
Go语言为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一个抽象类型。当你看到一个接口类型是,你不知道他是什么,唯一知道的是通过它的方法能做什么。
2.3 接口定义
Go语言提倡面向接口编程。
- 接口在底层实现上包含两部分,即类型(type)和数据(data)。
- 接口是一个或多个方法签名的集合
- 任何类型的方法集中只要拥有该接口对应的全部方法,就表示它实现了该接口,无须在该类型上显示声明实现了那个接口。这称为Structural Typing。
- 所谓对应方法,是指有相同名称,参数列表(不包括参数名)以及返回值。
- 当然,该类型还可以有其它方法。
- 接口只有方法声明,没有实现,没有数据字段(属性)。
- 接口可以匿名嵌入其它接口,或者嵌入到其它结构中。
- 对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,即无法修改复制品的状态,也无法获取指针。即如果对象是结构体或者基本数据类型,它会被值拷贝到接口中。如果对象是指针类型,这个指针指向的结构体实现了接口,那么接口中存储的是指针的副本,而不是指针本身。
- 只有当接口存储的类型和对象都为nil时,接口才为nil。
- 接口调用不会做receiver的自动转换。
- 接口同样支持匿名字段方法。
- 接口可以实现类似面向对象编程(OOP)的多态。
- 空接口可以作为任何类型数据的容器。空接口是没有声明任何方法的接口。但是,也无法通过空接口来调用对象的方法或访问其属性。
- 一个类型可以实现多个接口。
- 接口命名习惯以er结尾。
每一个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{方法1(参数列表1)返回值列表1方法2(参数列表2)返回值列表2...
}
其中:
- 接口名:使用type将接口名定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,接口名最好要能突出该接口的类型含义。
- 方法名:当方法名首字母是大写且接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表和返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子:
type writer interface{Write([]byte) error
}
当看到这个接口的时候,你不知道他是什么,唯一知道的是可以通过它的Write方法来做一些事情。
2.4 实现接口的条件
一个对象只要全部实现了接口的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
package mainimport "fmt"type Sayer interface {Say()
}type Cat struct{}//实现了Sayer接口
func (c *Cat) Say() {fmt.Println("喵喵喵...")
}type Dog struct{}//实现了Sayer接口
func (d *Dog) Say() {fmt.Println("汪汪汪...")
}
2.5 接口类型变量
接口类型变量能够存储所有实现了该接口的实例。
需要指针传入是因为方法的receiver为指针类型,对象的指针类型(*T)的方法集为值类型(T)和指针类型(*T)。
2.6 值接收者和指针接收者实现接口的区别
值接收者和指针接收者实现接口区别在于:方法集的不同。接口接收值类型(T)对象方法集为值接收者(T)实现的方法。接口接收指针类型对象(*T)方法集为值接收者(T)和指针接收者(*T)现象的方法。
- 值接收者
- 指针接收者
2.7 类型与接口的关系
2.7.1 一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
2.7.2 多个类型实现同一个接口
Go语言中不同的类型还可以实现同一接口。
一个接口的方法不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其它类型或者结构体来实现。
2.7.3 接口嵌套
接口与接口之间可以通过嵌套创造出新的接口。
type Sayer interface {Say()
}type Mover interface {Move()
}type Animal interface {SayerMover
}
嵌套的接口的使用和普通接口一样,这里实现嵌套的接口。
2.8 空接口
2.8.1 空接口的定义
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型变量可以存储任意类型的变量。
2.8.2 空接口的应用
-
空接口作为函数参数
使用空接口实现可以接收任意类型的函数参数。
- 空接口作为map的值
使用空接口实现可以保存任意值的字典。
2.8.3 类型断言
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
- 接口值
一个接口的值(简称接口值)是由一个具体类型和具体类型的值俩个部分组成。这两部分分别称为接口的动态类型和动态值。
我们来看一个例子:
package mainimport ("bytes""io""os"
)func main() {var w io.Writerw = os.Stdoutw = new(bytes.Buffer)w = nil
}
图解:
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
- x:表示类型为interface{}的变量
- T :表示断言x可能是的类型
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个是布尔值,若为true表示断言成功,为false则表示断言失败。
举个例子:
自定义类型:
我们还可以使用switch语句来实现多个类型的断言:
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中使用十分广泛。
但是接口需要注意的是,只有当有两个或者两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样会增加不必要的抽象,导致不必要的运行时消耗。
三. 接口的底层实现
- 案例
package maintype EInterface interface{}
type IInterface interface {Do()
}type IInterfaceImpl struct{}func (imp1 IInterfaceImpl) Do() {}func main() {var impl1 EInterface = IInterfaceImpl{}var impl2 IInterface = IInterfaceImpl{}println(impl1)println(impl2)
}
3.1 数据结构
golang中的接口非为带方法的接口和不带方法的空接口,带方法的接口在底层使用iface表示,空接口的底层则是eface表示。
- eface
eface是空接口类型的底层实现,源码如下:
type eface struct {_type *_typedata unsafe.Pointer
}
两个字段都是指针类型,含义分别是:
- _type:指向实际的类型。上面案例是IInterfaceImpl
- data:指向实际的值。上面案例是IInterfaceImpl结构体的值
注意:var v interface{} = (*int)nil,变量v其实使用的eface结构表示。其中_type的类型对应的是int类型的指针,而data部分为nil,所以整体变量v != nil。
- iface
iface是非空接口类型的底层实现,源码如下:
type iface struct {tab *itabdata unsafe.Pointer
}
上面的案例impl2就是非空接口类型的变量,两个字段也是指针类型,含义分别是:
- tab:指向itab结构体,itab结构体存储了接口所有方法列表。
- data:指向对应的值。上面案例为IInterfaceImpl结构体的值。
- _type结构
该结构于golang的类型系统有关,无论是内置类型还是自定义数据类型,都用_type结构表示其元信息。
type _type struct {size uintptrptrdata uintptr // size of memory prefix holding all pointershash uint32tflag tflagalign uint8fieldAlign uint8kind uint8// function for comparing objects of this type// (ptr to object A, ptr to object B) -> ==?equal func(unsafe.Pointer, unsafe.Pointer) bool// gcdata stores the GC type data for the garbage collector.// If the KindGCProg bit is set in kind, gcdata is a GC program.// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.gcdata *bytestr nameOffptrToThis typeOff
}
以int32类型的指针为例,具体的字段含义及其作用如下:
- size:类型的大小(字节数)。*int32类型的大小是64位系统下8字节,32位系统下4字节。
- ptrdata:所有指针内存前缀大小,指向int32类型的实例。
- hash:类型的哈希值,即_type.hash。
- tflag:类型标记,表示类型的特性。
- align:类型的对齐方式,*int32类型32位系统下按4字节对齐,64位系统下8字节对齐。
- fieldAlign:字段对齐方式,*int32类型的字段对齐方式 32位系统下按4字节对齐,64位系统下8字节对齐。
- kind:类型的种类。用于区分基本类型,结构体,接口等。
- equal:比较函数,用于区分两个*int32类型的实例是否相等。
- gcdata:与GC有关。
- str:类型名称的偏移。
- ptrToThis:指向该类型的指针。
_type是一个很复杂的结构,这里只需要知道,通过该结构能获取到结构体实现的所有方法。
下面是iface.go中的init方法中的一段代码,_type结构的uncommon方法会返回一个指针,在此基础上加一个偏移量(moff)就能得到实际结构体实现的方法列表。
func (m *itab) init() string {inter := m.intertyp := m._typex := typ.uncommon()// both inter and typ have method sorted by name,// and interface names are unique,// so can iterate over both in lock step;// the loop is O(ni+nt) not O(ni*nt).ni := len(inter.mhdr)nt := int(x.mcount)// 实际类型的方法数组xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]...
}
- itab结构
itab结构是golang非空接口iface中一个非常重要的字段,类型的赋值,断言等都离不开该字段。
type itab struct {// 接口类型的指针,比如对于 io.Reader 接口,记录的是接口类型的信息(如接口定义的方法,Read 方法)inter *interfacetype// 实际结构类型的指针,记录的是实际类型的信息,比如 os.File 类型,实现了 io.Reader 接口_type *_typehash uint32 // copy of _type.hash. Used for type switches._ [4]byte// 变长数组,fun[0]==0 表示 _type 没有实现 inter 接口fun [1]uintptr
}type interfacetype struct {// 接口类型元信息typ _type// 包路径pkgpath name// 接口的所有方法列表mhdr []imethod
}
itab结构的字段含义:
- inter记录的是非空接口类型的元信息,其中mhdr是接口的方法表
- _type记录的是实际类型的指针,即实现接口的类型
- fun保存的是实际类型中实现的方法的地址。当fun[0] == 0时,表示该类型没有实现该接口;当fun[0]!=0时,则代表该类型实现了接口的所有方法,这时候就可以通过偏移量调用具体类型对象的方法。
3.2 itab关键方法
通过上述对itab结构的描述,不难理解,itab其实就是一个缓存,用于快速判断具体类型是否实现了某个接口。
一般情况下,如果要判断需要对接口的具体类型的方法集进行比较。当如果每次都这样比较,效率会很低。通过将比较结果缓存起来,下次再判断的时候,就能直接根据itab快速得出结论了。
go源码的runtime里定义了全局变量itebTable,用户缓存itab。
// 用于缓存 itab
itabTable = &itabTableInit
itabTableInit = itabTableType{size: itabInitSize}// 全局的 itab 表
type itabTableType struct {size uintptr // entries 的长度count uintptr // 当前 entries 的数量,即 itab 数量entries [itabInitSize]*itab // 保存 itab 的哈希表
}
这里其实是一个全局的哈希表,哈希表的key就是interfacetype + _type,value就是对应的itab。
判断某个类型是否实现了接口时,只需要传入接口的接口类型interfacetype和实际类型_type即可:
- 如果在itabTable中没有找到对应的itab,则需要依次比较方法集,生成itab并缓存到itabTable中
- 如果找到了对应的itab,则判断func[0],如果等于0则说明该类型没有实现该接口
有了哈希表,还要考虑如何向其中添加数据和获取数据,也就是下面两个方法。
- getitab
该函数的作用是:通过interfacetype和_type,也就是接口类型和实际结构类型,从表中获取对应itab。
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {...var m *itab// 尝试从 itabTable 表中获取 itab,获取到直接返回t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))if m = t.find(inter, typ); m != nil {goto finish}// 没有找到,获取锁再次查找lock(&itabLock)if m = itabTable.find(inter, typ); m != nil {unlock(&itabLock)goto finish}// 如果 itabTable 中没有找到,则新建一个 itab,并调用 itabAdd 将其缓存到 itabTable 中m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*goarch.PtrSize, 0, &memstats.other_sys))m.inter = interm._type = typm.hash = 0m.init()itabAdd(m)unlock(&itabLock)
finish:// m.fun[0] != 0 表示该类型实现了接口的所有方法,可以返回 itabif m.fun[0] != 0 {return m}//canfail 用于控制类型转换失败的行为。比如 v := s.(Dst),这里的 canfail == false,那么在断言失败时,会 panicif canfail {return nil}panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
这里主要的逻辑是:
- 更具interfacetype和_type,尝试从itabTable中获取itab
- 如果itabTable没有找到itab,则新创建一个itab并将其缓存到itabTable中
- 判断该类型是否实现了接口的所有方法(m.fun[0]!=0)
- 如果该类型没有实现接口的所有方法,则根据canfail判断是否panic,canfail为false则会panic。比如类型断言时,如果不接受第二个返回值,则断言失败会panic
首次调用getitab方法获取时,哈希表中是没有对应数据的。此时不仅要创建itab结构,还要对其涉及到的接口和类型的方法集进行判断,初始化等。相关代码在init中。
func (m *itab) init() string {inter := m.intertyp := m._typex := typ.uncommon()// 接口定义的方法数量ni := len(inter.mhdr)// 实际类型的方法数量nt := int(x.mcount)// 实际类型的方法数组xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]j := 0// 保存接口的第i个方法对应的实际类型的方法的地址methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]var fun0 unsafe.Pointer
imethods:// 遍历接口方法列表for k := 0; k < ni; k++ {// 接口的方法i := &inter.mhdr[k]// 接口的方法类型itype := inter.typ.typeOff(i.ityp)// 接口的方法名称name := inter.typ.nameOff(i.name)// 接口的方法名iname := name.name()// 接口的包路径ipkg := name.pkgPath()if ipkg == "" {ipkg = inter.pkgpath.name()}// 根据接口方法查找实际类型的方法for ; j < nt; j++ {// 实际类型的方法t := &xmhdr[j]// 实际类型的方法名tname := typ.nameOff(t.name)// 比较接口的方法名和实际类型的方法是否一致,包括名称和类型if typ.typeOff(t.mtyp) == itype && tname.name() == iname {pkgPath := tname.pkgPath()if pkgPath == "" {pkgPath = typ.nameOff(x.pkgpath).name()}// 如果是导出方法或在同一个包,则将将方法保存到 itab 中if tname.isExported() || pkgPath == ipkg {if m != nil {// 实际类型的方法指针,通过该指针可以调用实际类型的方法ifn := typ.textOff(t.ifn)if k == 0 {fun0 = ifn // we'll set m.fun[0] at the end} else {methods[k] = ifn}}continue imethods}}}// 该类型没有实现接口// 如果每个接口方法都被实现了,则每次都会走到 continue 的逻辑,不会将 fuc[0] 置为 0m.fun[0] = 0return iname}m.fun[0] = uintptr(fun0)return ""
}
主要逻辑是,依次遍历接口的所有方法,并在实际类型的接口列表中查找对应的实现,只要有一个接口方法没有被实现,则将itab的fun[0]置为0,表示该类型没有实现该接口。
- itabadd
getitab方法如果没有找到itab,会新建一个itab并调用itabAdd方法将其缓存到itabTable中。
func itabAdd(m *itab) {...t := itabTable// 容量超过 75% 时会触发扩容if t.count >= 3*(t.size/4) { // 75% load factor// 扩容为原哈希表的 2 倍大小t2 := (*itabTableType)(mallocgc((2+2*t.size)*goarch.PtrSize, nil, true))t2.size = t.size * 2// 将原哈希表的元素复制到新哈希表// 复制过程中,其他的线程可能会尝试从原哈希表中获取 itab,但找不到。此时会尝试获取锁(会阻塞)后再次获取。iterate_itabs(t2.add)-】if t2.count != t.count {throw("mismatched count during itab table copy")}// 使用原子操作将 itabTable 的引用指向新扩容的内存atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))// Adopt the new table as our own.t = itabTable// Note: the old table can be GC'ed here.}// 将新的 itab 缓存到 itabTable 中t.add(m)
}
3.3 接口赋值
将某一具体类型赋值给接口类型时,本质其实时如何填充eface和iface结构体。
对eface结构体来说,由于只有_type和data字段,因此只需要进行字段赋值即可。
对于iface结构体来说,需要通过itab判断类型值是否实现了接口的所有方法(itab可能不存在,会走一遍getitab的流程),然后初始化iface结构的tab和data字段。
底层会调用runtime.convTXXX转换为iface或eface的data字段。
func convT(t *_type, v unsafe.Pointer) unsafe.Pointer {...// 分配`_type`所需的内存x := mallocgc(t.size, t, true)// 将v的值复制到刚分配的内存typedmemmove(t, x, v)return x
}