interface
Publish on 2024-11-24

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

Implicit Implementation of Receiver

Methods ReceiversValues
(t T)T and *T
(t *T)*T

上面的表格可以解读为:如果是值Receiver,实体类型的值和指针都可以实现对应的接口;如果是指针Receiver,那么只有类型的指针能够实现对应的接口。

其次我们我们以实体类型是值还是指针的角度看。

ValuesMethods Receivers
T(t T)
*T(t T) and (t *T)

上面的表格可以解读为:类型的值只能实现值Receiver的接口;指向类型的指针,既可以实现值Receiver的接口,也可以实现指针Receiver的接口。

总结一下:想要创建接口类型的对象(如下方的coder接口),可以用***T的值来初始化接口对象,因为不管*T的指针对象实现的接口方法中Receiver是T还是*T。都可以认为是实现了接口。但是,T****的对象实现接口方法时,只有Receiver都是值Receiver才可以认为是实现了接口方法。**或者更宏观地说,指针可以通过解引用获得值,但是值不应该取地址去调用指针的方法。

这样的设计有一个简单的解释:接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响

下面是一个简单的 修改对象/副本 的案例:

package main

type coder interface {
	code()
	debug()
}

type Gopher struct {
	language string
}

func (p *Gopher) code() {
	p.language = "Go"
}

func (p Gopher) debug() {
	p.language = "Rust"
}
func main() {
	var c coder = &Gopher{"C"}
	c.debug()
	println(c.(*Gopher).language)
	c.code()
	println(c.(*Gopher).language)
}

只要是值Receiver,不管调用者是什么,只会修改对象的副本,只有指针Receiver会修改对象。

以上内容为接口的特性,勿和函数调用混淆。

iface & eface

iface (interface)

从源码层面看一下:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

// 旧版本itab
~~type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}~~

// 新版本itab
type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。

再来仔细看一下 itab 结构体:_type 字段描述了实体的类型,包括内存对齐方式,大小等;inter 字段则描述了接口的类型。fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。

为什么 fun 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。

再看一下 interfacetype 类型,它描述的是接口的类型:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。

这里通过一张图来看下 iface 结构体的全貌:

Untitled.png

eface (empty interface)

接着来看一下 eface 的源码:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

相比 ifaceeface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

Untitled.png

我们来看个例子:

package main

import "fmt"

func main() {
    x := 200
    var any interface{} = x
    fmt.Println(any)

    g := Gopher{"Go"}
    var c coder = g
    fmt.Println(c)
}

type coder interface {
    code()
    debug()
}

type Gopher struct {
    language string
}

func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}

func (p Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}

执行命令,打印出汇编语言:

go tool compile -S ./src/main.go

可以看到,main 函数里调用了两个函数:

func convT2E64(t *_type, elem unsafe.Pointer) (e eface)
func convT2I(tab *itab, elem unsafe.Pointer) (i iface)

上面两个函数的参数和 ifaceeface 结构体的字段是可以联系起来的:两个函数都是将参数组装一下,形成最终的接口。

作为补充,我们最后再来看下 _type 结构体:

type _type struct {
    // 类型大小
    size       uintptr
    ptrdata    uintptr
    // 类型的 hash 值
    hash       uint32
    // 类型的 flag,和反射相关
    tflag      tflag
    // 内存对齐相关
    align      uint8
    fieldalign uint8
    // 类型的编号,有bool, slice, struct 等等等等
    kind       uint8
    alg        *typeAlg
    // gc 相关
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段来进行管理的:

type arraytype struct {
    typ   _type
    elem  *_type
    slice *_type
    len   uintptr
}

type chantype struct {
    typ  _type
    elem *_type
    dir  uintptr
}

type slicetype struct {
    typ  _type
    elem *_type
}

type structtype struct {
    typ     _type
    pkgPath name
    fields  []structfield
}

这些数据类型的结构体定义,是反射实现的基础。

什么是itab?

iface中tab字段,是一個itab結構,包含了inter接口類型、_type數據類型、hash哈希的方法、fun函數地址佔位符。這個hash方法拷貝自_type.hash;fun是一個大小爲1的uintptr數組,當fun[0]爲0時,說明_type並沒有實現該接口,當有實現接口時,fun存放了第一個接口方法的地址,其他方法一次往下存放,這裏就簡單用空間換時間,其實方法都在_type字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了

全局的itab table

iface.go:

const itabInitSize = 512

// Note: change the formula in the mallocgc call in itabAdd if you change these fields.
type itabTableType struct {
    size    uintptr             // length of entries array. Always a power of 2.
    count   uintptr             // current number of filled entries.
    entries [itabInitSize]*itab // really [size] large
}

可以看出這個全局的itabTable是用數組在存儲的;size記錄數組的大小,總是2的次冪;count記錄數組中已使用了多少;entries是一個*itab數組,初始大小是512。

獲取itab的流程

golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。下面我們看下代碼:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    if len(inter.mhdr) == 0 {
        throw("internal error - misuse of itab")
    }

    // easy case
    if typ.tflag&tflagUncommon == 0 {
        if canfail {
            return nil
        }
        name := inter.typ.nameOff(inter.mhdr[0].name)
        panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
    }

    var m *itab

    // First, look in the existing table to see if we can find the itab we need.
    // This is by far the most common case, so do it without locks.
    // Use atomic to ensure we see any previous writes done by the thread
    // that updates the itabTable field (with atomic.Storep in itabAdd).
    t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
    if m = t.find(inter, typ); m != nil {
        goto finish
    }

    // Not found.  Grab the lock and try again.
    lock(&itabLock)
    if m = itabTable.find(inter, typ); m != nil {
        unlock(&itabLock)
        goto finish
    }

    // Entry doesn't exist yet. Make a new entry & add it.
    m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
    m.inter = inter
    m._type = typ
    m.init()
    itabAdd(m)
    unlock(&itabLock)
finish:
    if m.fun[0] != 0 {
        return m
    }
    if canfail {
        return nil
    }
    // this can only happen if the conversion
    // was already done once using the , ok form
    // and we have a cached negative result.
    // The cached result doesn't record which
    // interface function was missing, so initialize
    // the itab again to get the missing function name.
    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

流程很簡單

  1. 先用t保存全局itabTable的地址,然後使用t.find去查找,這樣是爲了防止查找過程中,itabTable被替換導致查找錯誤。
  2. 如果沒找到,那麼就會上鎖,然後使用itabTable.find去查找,這樣是因爲在第一步查找的同時,另外一個協程寫入,可能導致實際存在卻查找不到,這時上鎖避免itabTable被替換,然後直接在itaTable中查找。
  3. 再沒找到,說明確實沒有,那麼就根據接口類型、數據類型,去生成一個新的itab,然後插入到itabTable中,這裏可能會導致hash表擴容,如果數據類型並沒有實現接口,那麼根據調用方式,該報錯報錯,該panic panic。

這裏我們可以看到申請新的itab空間時,內存空間大小 unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結構,len(inter.mhdr)就是接口定義的方法數量,因爲字段fun是一個大小爲1的數組,所以len(inter.mhdr)-1,在fun字段下面其實隱藏了其他方法接口地址。

接口和nil

接口值的零值是指动态类型动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil

来看个例子:

package main

import "fmt"

type Coder interface {
    code()
}

type Gopher struct {
    name string
}

func (g Gopher) code() {
    fmt.Printf("%s is coding\n", g.name)
}

func main() {
    var c Coder
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)

    var g *Gopher
    fmt.Println(g == nil)

    c = g
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
}

输出:

true
c: <nil>, <nil>
true
false
c: *main.Gopher, <nil>

一开始,c 的 动态类型和动态值都为 nilg 也为 nil,当把 g 赋值给 c 后,c 的动态类型变成了 *main.Gopher,仅管 c 的动态值仍为 nil,但是当 cnil 作比较的时候,结果就是 false 了。

返回值为接口时的隐式转换

来看一个例子,看一下它的输出:

package main

import "fmt"

type MyError struct {}

func (i MyError) Error() string {
    return "MyError"
}

func main() {
    err := Process()
    fmt.Println(err)

    fmt.Println(err == nil)
}

func Process() error {
    var err *MyError = nil
    return err
}

函数运行结果:

<nil>
false

这里先定义了一个 MyError 结构体,实现了 Error 函数,也就实现了 error 接口。Process 函数返回了一个 error 接口,这块隐含了类型转换。所以,虽然它的值是 nil,其实它的类型是 *MyError,最后和 nil 比较的时候,结果为 false

让err成为结构体对象而非指针对象:

func Process() error {
    var err MyError = MyError{}
    return err
}
或者
func Process() error {
	var err *MyError = &MyError{}
	return err
}

函数运行结果:

MyError
false

所以,当不在乎值类型的时候,应该直接对空结构体指针赋nil,可以让代码逻辑更清晰

打印接口的动态类型和值

package main

import (
    "unsafe"
    "fmt"
)

type iface struct {
    itab, data uintptr
}

func main() {
    var a interface{} = nil

    var b interface{} = (*int)(nil)

    x := 5
    var c interface{} = (*int)(&x)

    ia := *(*iface)(unsafe.Pointer(&a))
    ib := *(*iface)(unsafe.Pointer(&b))
    ic := *(*iface)(unsafe.Pointer(&c))

    fmt.Println(ia, ib, ic)

    fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}

代码里直接定义了一个 iface 结构体,用两个指针来描述 itabdata,之后将 a, b, c 在内存中的内容强制解释成我们自定义的 iface。最后就可以打印出动态类型和动态值的地址。

运行结果如下:

{0 0} {17426912 0} {17426912 842350714568}
5

a 的动态类型和动态值的地址均为 0,也就是 nil;b 的动态类型和 c 的动态类型一致,都是 *int;最后,c 的动态值为 5。

编译器检测接口是否实现

经常看到一些开源库里会有一些类似下面这种奇怪的用法:

var _ io.Writer = (*myWriter)(nil)

这时候会有点懵,不知道作者想要干什么,实际上这就是此问题的答案。编译器会由此检查 *myWriter 类型是否实现了 io.Writer 接口。

来看一个例子:

package main

import "io"

type myWriter struct {

}

/*func (w myWriter) Write(p []byte) (n int, err error) {
    return
}*/

func main() {
    // 检查 *myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = (*myWriter)(nil)

    // 检查 myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = myWriter{}
}

注释掉为 myWriter 定义的 Write 函数后,运行程序:

src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment:
    *myWriter does not implement io.Writer (missing Write method)
src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment:
    myWriter does not implement io.Writer (missing Write method)

报错信息:*myWriter/myWriter 未实现 io.Writer 接口,也就是未实现 Write 方法。

解除注释后,运行程序不报错。

实际上,上述赋值语句会发生隐式地类型转换,在转换的过程中,编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数。

总结一下,可通过在代码中添加类似如下的代码,用来检测类型是否实现了接口:

var _ io.Writer = (*myWriter)(nil)
var _ io.Writer = myWriter{}

接口的构造

package main

import "fmt"

type Person interface {
	growUp()
}

type Student struct {
	age int
}

func (p Student) growUp() {
	p.age += 1
	return
}

func main() {
	var qcrao = Person(Student{age: 18})

	fmt.Println(qcrao)
}

通过汇编可以看到调用了以下函数

func convT64(val uint64) (x unsafe.Pointer) {
	if val < uint64(len(staticuint64s)) {
		x = unsafe.Pointer(&staticuint64s[val])
	} else {
		x = mallocgc(8, uint64Type, false)
		*(*uint64)(x) = val
	}
	return
}
rel 2+0 t=23 type:main.Student+0
rel 2+0 t=23 type:*os.File+0
rel 35+4 t=7 runtime.convT64+0
rel 48+4 t=14 go:itab.main.Student,main.Person+8
rel 65+4 t=14 os.Stdout+0
rel 72+4 t=14 go:itab.*os.File,io.Writer+0
rel 90+4 t=7 fmt.Fprintln+0
rel 105+4 t=7 runtime.morestack_noctxt+0

需要注意的是,新版本的itab有所修改,size由40Byte变为32Byte

type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

如何打印出接口类型的 Hash

具体做法如下:

package main

import (
	"fmt"
	"unsafe"
)

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

type itab struct {
	inter uintptr
	_type uintptr
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

type Person interface {
	growUp()
}

type Student struct {
	age int
}

func (p Student) growUp() {
	p.age += 1
	return
}
func main() {
	var qcrao = Person(Student{age: 18})
	var pcrao = Person(Student{age: 177})
	ifaceq := (*iface)(unsafe.Pointer(&qcrao))
	ifacep := (*iface)(unsafe.Pointer(&pcrao))
	fmt.Printf("iface.tab.hash = %#x\n", ifaceq.tab.hash)
	fmt.Printf("iface.tab.hash = %#x\n", ifacep.tab.hash)
}

定义了一个山寨版的 ifaceitab,说它山寨是因为 itab 里的一些关键数据结构都不具体展开了,比如 _type,对比一下正宗的定义就可以发现,但是山寨版依然能工作,因为 _type 就是一个指针而已嘛。

main 函数里,先构造出一个接口对象 qcrao,然后强制类型转换,最后读取出 hash 值。

运行结果:

➜ go run ./ifacehash.go
iface.tab.hash = 0x7ab929eb
iface.tab.hash = 0x7ab929eb

值得一提的是,构造接口 qcrao 的时候,即使我把 age 写成其他值,得到的 hash 值依然不变的,这应该是可以预料的,hash 值只和他的字段、方法相关。

接口转换

通过前面提到的 iface 的源码可以看到,实际上它包含接口的类型 interfacetype 和 实体类型的类型 _type,这两者都是 iface 的字段 itab 的成员。也就是说生成一个 itab 同时需要接口的类型和实体的类型。

<interface 类型, 实体类型> ->itable

当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。

例如某类型有 m 个方法,某接口有 n 个方法,则很容易知道这种判定的时间复杂度为 O(mn)****,Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)****。

直接来看一个例子:

package main

import "fmt"

type coder interface {
    code()
    run()
}

type runner interface {
    run()
}

type Gopher struct {
    language string
}

func (g Gopher) code() {
    return
}

func (g Gopher) run() {
    return
}

func main() {
    var c coder = Gopher{}

    var r runner
    r = c
    fmt.Println(c, r)
}

简单解释下上述代码:定义了两个 interface: coderrunner。定义了一个实体类 Gopher,类型 Gopher 实现了两个方法,分别是 run()code()。main 函数里定义了一个接口变量 c,绑定了一个 Gopher 对象,之后将 c 赋值给另外一个接口变量 r 。赋值成功的原因是 c 中包含 run() 方法。这样,两个接口变量完成了转换。

执行命令:

go tool compile -S ./src/main.go

得到 main 函数的汇编命令,可以看到: r = c 这一行语句实际上是调用runtime.convI2I(SB),也就是 convI2I 函数,从函数名来看,就是将一个 interface 转换成另外一个 interface,看下它的源代码:

func convI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {
        return
    }
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}

代码比较简单,函数参数 inter 表示接口类型,i 表示绑定了实体类型的接口,r 则表示接口转换了之后的新的 iface。通过前面的分析,我们又知道, iface 是由 tabdata 两个字段组成。所以,实际上 convI2I 函数真正要做的事,找到新 interfacetabdata,就大功告成了。

我们还知道,tab 是由接口类型 interfacetype 和 实体类型 _type。所以最关键的语句是 r.tab = getitab(inter, tab._type, false)

因此,重点来看下 getitab 函数的源码,只看关键的地方:

golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。下面我們看下代碼:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    if len(inter.mhdr) == 0 {
        throw("internal error - misuse of itab")
    }

    // easy case
    if typ.tflag&tflagUncommon == 0 {
        if canfail {
            return nil
        }
        name := inter.typ.nameOff(inter.mhdr[0].name)
        panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
    }

    var m *itab

    // First, look in the existing table to see if we can find the itab we need.
    // This is by far the most common case, so do it without locks.
    // Use atomic to ensure we see any previous writes done by the thread
    // that updates the itabTable field (with atomic.Storep in itabAdd).
    t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
    if m = t.find(inter, typ); m != nil {
        goto finish
    }

    // Not found.  Grab the lock and try again.
    lock(&itabLock)
    if m = itabTable.find(inter, typ); m != nil {
        unlock(&itabLock)
        goto finish
    }

    // Entry doesn't exist yet. Make a new entry & add it.
    m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
    m.inter = inter
    m._type = typ
    m.init()
    itabAdd(m)
    unlock(&itabLock)
finish:
    if m.fun[0] != 0 {
        return m
    }
    if canfail {
        return nil
    }
    // this can only happen if the conversion
    // was already done once using the , ok form
    // and we have a cached negative result.
    // The cached result doesn't record which
    // interface function was missing, so initialize
    // the itab again to get the missing function name.
    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

流程很簡單

  1. 先用t保存全局itabTable的地址,然後使用t.find去查找,這樣是爲了防止查找過程中,itabTable被替換導致查找錯誤。
  2. 如果沒找到,那麼就會上鎖,然後使用itabTable.find去查找,這樣是因爲在第一步查找的同時,另外一個協程寫入,可能導致實際存在卻查找不到,這時上鎖避免itabTable被替換,然後直接在itaTable中查找。
  3. 再沒找到,說明確實沒有,那麼就根據接口類型、數據類型,去生成一個新的itab,然後插入到itabTable中,這裏可能會導致hash表擴容,如果數據類型並沒有實現接口,那麼根據調用方式,該報錯報錯,該panic panic。

這裏我們可以看到申請新的itab空間時,內存空間大小 unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結構,len(inter.mhdr)就是接口定義的方法數量,因爲字段fun是一個大小爲1的數組,所以len(inter.mhdr)-1,在fun字段下面其實隱藏了其他方法接口地址。

再来看一下 itabAdd 函数的代码:

func itabAdd(m *itab) {
	// Bugs can lead to calling this while mallocing is set,
	// typically because this is called while panicking.
	// Crash reliably, rather than only when we need to grow
	// the hash table.
	if getg().m.mallocing != 0 {
		throw("malloc deadlock")
	}

	t := itabTable
	if t.count >= 3*(t.size/4) { // 75% load factor
		// Grow hash table.
		// t2 = new(itabTableType) + some additional entries
		// We lie and tell malloc we want pointer-free memory because
		// all the pointed-to values are not in the heap.
		t2 := (*itabTableType)(mallocgc((2+2*t.size)*goarch.PtrSize, nil, true))
		t2.size = t.size * 2

		// Copy over entries.
		// Note: while copying, other threads may look for an itab and
		// fail to find it. That's ok, they will then try to get the itab lock
		// and as a consequence wait until this copying is complete.
		iterate_itabs(t2.add)
		if t2.count != t.count {
			throw("mismatched count during itab table copy")
		}
		// Publish new hash table. Use an atomic write: see comment in getitab.
		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.
	}
	t.add(m)
}

itabAdd 会检查 m.mallocing标志位,然后检查itabTable的负载因子。如果大于阈值3/4就扩容2倍并复制所有的旧的itabt2指针指向的内存,最后进行一个指针的换,丢弃原来的itabTable ,最后再添加新的m *itab

新版本中itabTable不再是哈希表,而是切片,itabTable.find(inter, typ)是怎么做的:

func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
	// Implemented using quadratic probing.
	// Probe sequence is h(i) = h0 + i*(i+1)/2 mod 2^k.
	// We're guaranteed to hit all table entries using this probe sequence.
	mask := t.size - 1
	h := itabHashFunc(inter, typ) & mask
	for i := uintptr(1); ; i++ {
		p := (**itab)(add(unsafe.Pointer(&t.entries), h*goarch.PtrSize))
		// Use atomic read here so if we see m != nil, we also see
		// the initializations of the fields of m.
		// m := *p
		m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
		if m == nil {
			return nil
		}
		if m.inter == inter && m._type == typ {
			return m
		}
		h += i
		h &= mask
	}
}

itabHashFunc会搞出一个哈希值,具体值是intertyp相与。这个结果再和mask相与,获得一个范围在[0, t.size]之间的值。然后使用 quadratic probing 进行hash插入,如果想refer to有关hash的内容:All I need to know about basic hash

更一般的,当把实体类型赋值给接口的时候,会调用 conv 系列函数,例如空接口调用 convT2E 系列、非空接口调用 convT2I 系列。这些函数比较相似:

具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。

类型转换和断言

我们知道,Go 语言中不允许隐式类型转换,也就是说 = 两边,不允许出现类型不相同的变量。

类型转换类型断言本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。

类型转换

对于类型转换而言,转换前后的两个类型要相互兼容才行。类型转换的语法为:

<结果类型> := <目标类型> ( <表达式> )

package main

import "fmt"

func main() {
    var i int = 9

    var f float64
    f = float64(i)
    fmt.Printf("%T, %v\n", f, f)

    f = 10.8
    a := int(f)
    fmt.Printf("%T, %v\n", a, a)

    // s := []int(i)
}

上面的代码里,我定义了一个 int 型和 float64 型的变量,尝试在它们之前相互转换,结果是成功的:int 型和 float64 是相互兼容的。

如果我把最后一行代码的注释去掉,编译器会报告类型不兼容的错误:

cannot convert i (type int) to type []int

断言

前面说过,因为空接口 interface{} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

断言的语法为:

<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言

类型转换和类型断言有些相似,不同之处,在于类型断言是对接口进行的操作。

还是来看一个简短的例子:

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func main() {
    var i interface{} = new(Student)
    s := i.(Student)

    fmt.Println(s)
}

运行一下:

panic: interface conversion: interface {} is *main.Student, not main.Student

直接 panic 了,这是因为 i*Student 类型,并非 Student 类型,断言失败。这里直接发生了 panic,线上代码可能并不适合这样做,可以采用“安全断言”的语法:

func main() {
    var i interface{} = new(Student)
    s, ok := i.(Student)
    if ok {
        fmt.Println(s)
    }
}

这样,即使断言失败也不会 panic

断言其实还有另一种形式,就是用在利用 switch 语句判断接口的类型。每一个 case 会被顺序地考虑。当命中一个 case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case 匹配的情况。

代码示例如下:

func main() {
    //var i interface{} = new(Student)
    //var i interface{} = (*Student)(nil)
    var i interface{}

    fmt.Printf("%p %v\n", &i, i)

    judge(i)
}

func judge(v interface{}) {
    fmt.Printf("%p %v\n", &v, v)

    switch v := v.(type) {
    case nil:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("nil type[%T] %v\n", v, v)

    case Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("Student type[%T] %v\n", v, v)

    case *Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("*Student type[%T] %v\n", v, v)

    default:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("unknow\n")
    }
}

type Student struct {
    Name string
    Age int
}

main 函数里有三行不同的声明,每次运行一行,注释另外两行,得到三组运行结果:

// --- var i interface{} = new(Student)
0xc4200701b0 [Name: ], [Age: 0]
0xc4200701d0 [Name: ], [Age: 0]
0xc420080020 [Name: ], [Age: 0]
*Student type[*main.Student] [Name: ], [Age: 0]

// --- var i interface{} = (*Student)(nil)
0xc42000e1d0 <nil>
0xc42000e1f0 <nil>
0xc42000c030 <nil>
*Student type[*main.Student] <nil>

// --- var i interface{}
0xc42000e1d0 <nil>
0xc42000e1e0 <nil>
0xc42000e1f0 <nil>
nil type[<nil>] <nil>

对于第一行语句:

var i interface{} = new(Student)

i 是一个 *Student 类型,匹配上第三个 case,从打印的三个地址来看,这三处的变量实际上都是不一样的。在 main 函数里有一个局部变量 i;调用函数时,实际上是复制了一份参数,因此函数里又有一个变量 v,它是 i 的拷贝;断言之后,又生成了一份新的拷贝。所以最终打印的三个变量的地址都不一样。

对于第二行语句:

var i interface{} = (*Student)(nil)

这里想说明的其实是 i 在这里动态类型是 (*Student), 数据为 nil,它的类型并不是 nil,它与 nil 作比较的时候,得到的结果也是 false

最后一行语句:

var i interface{}

这回 i 才是 nil 类型。

【引申1】 fmt.Println 函数的参数是 interface。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 String() 方法,如果实现了,则直接打印输出 String() 方法的结果;否则,会通过反射来遍历对象的成员进行打印。

再来看一个简短的例子,比较简单,不要紧张:

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func main() {
    var s = Student{
        Name: "qcrao",
        Age: 18,
    }

    fmt.Println(s)
}

因为 Student 结构体没有实现 String() 方法,所以 fmt.Println 会利用反射挨个打印成员变量:

{qcrao 18}

增加一个 String() 方法的实现:

func (s Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

打印结果:

[Name: qcrao], [Age: 18]

按照我们自定义的方法来打印了。

【引申2】 针对上面的例子,如果改一下:

func (s *Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

注意看两个函数的接受者类型不同,现在 Student 结构体只有一个接受者类型为 指针类型String() 函数,打印结果:

{qcrao 18}

为什么?

类型 T 只有接受者是 T 的方法;而类型 *T 拥有接受者是 T 和 *T 的方法。语法上 T 能直接调 *T 的方法仅仅是 Go 的语法糖。

所以, Student 结构体定义了接受者类型是值类型的 String() 方法时,通过

fmt.Println(s)
fmt.Println(&s)

均可以按照自定义的格式来打印。

如果 Student 结构体定义了接受者类型是指针类型的 String() 方法时,只有通过

fmt.Println(&s)

才能按照自定义的格式打印。

接口与纯虚函数

接口定义了一种规范,描述了类的行为和功能,而不做具体实现。

C++ 的接口是使用抽象类来实现的,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 ”= 0” 来指定的。例如:

class Shape
{
   public:
      // 纯虚函数
      virtual double getArea() = 0;
   private:
      string name;      // 名称
};

设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。

派生类需要明确地声明它继承自基类,并且需要实现基类中所有的纯虚函数。

C++ 定义接口的方式称为“侵入式”,而 Go 采用的是 “非侵入式”,不需要显式声明,只需要实现接口定义的函数,编译器自动会识别。

C++ 和 Go 在定义接口方式上的不同,也导致了底层实现上的不同。C++ 通过虚函数表来实现基类调用派生类的函数;而 Go 通过 itab 中的 fun 字段来实现接口变量调用实体类型的函数。C++ 中的虚函数表是在编译期生成的;而 Go 的 itab 中的 fun 字段是在运行期间动态生成的。原因在于,Go 中实体类型可能会无意中实现 N 多接口,很多接口并不是本来需要的,所以不能为类型实现的所有接口都生成一个 itab****, 这也是“非侵入式”带来的影响;这在 C++ 中是不存在的,因为派生需要显示声明它继承自哪个基类。

© 2024 humbornjo :: based on 
nobloger  ::  rss