golang之interface学习总结

it2023-05-24  63

概述

本篇目的是对go中的interface做一个总结。 主要参考https://qcrao91.gitbook.io/go/interface,

环境说明

$ uname -a Linux gl.com 5.4.50-amd64-desktop #74 SMP Mon Aug 24 20:15:37 CST 2020 x86_64 GNU/Linux $ go version go version go1.15.2 linux/amd64

一个例子

func basic() { var i interface{} var r = func() *int { return nil }() i = r fmt.Println(i == nil, r == nil) }

一看题目:啊, 就这? 一看答案:啊, 这? 上述代码输出为: false true,r == nil为true很好理解, 但是这个i == nil为false就很让人费解了。 要了解为什么, 需要学一下interface的原理。 当然本篇不只是为了解答上面的问题, 目的是关于interface做一个比较全面的一个总结, 所以一步步来。

值接收者和指针接收者的区别

一句话:不管方法的接收者是什么类型,该类型的值和指针都可以调用(编译运行都没语法问题),不必严格符合接收者的类型。, 如:

// receiver.go import "fmt" type Animal struct { name string age int } func (a Animal) Name(name string) { a.name = name } func (a *Animal) Age(age int) { a.age = age } func (a Animal) String() string { return fmt.Sprintf("I'm %s, %d years old", a.name, a.age) } // main.go // I'm , 3 years old // I'm , 4 years old func fn_receiver() { var a Animal a.Name("lion") a.Age(3) fmt.Println(a) b := &Animal{} b.Name("tiger") b.Age(4) fmt.Println(b) }

可以看到, 上述不论以指针还是值来调func (a Animal) Name(name string), 都没有改变name的值; 上述不论以指针还是值来调func (a *Animal) Age(age int), 都可以改变age的值;

事实上, 当调用者类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作; 如果你用指针去调用值接收者(上述的b.Name("tiger")), 背后其实是(*b).Name("tiger"); 用值去调指针接收者时(上述的a.Age(3)), 其实背后是(&a).Age(3). 还有就是为什么name的值为啥怎么都改不了呢? 因为func (a Animal) Name(name string)这里不是a *Animal, 不管你怎么调用, 都是调用者对应的实际的struct内存的拷贝,所以改不了。

继续 实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。 仔细读下, 如果不懂, 请看下面的例子:

// pointer_vs_value_receiver.go import "fmt" type People interface { Say(string) } type Gopher struct { } // cannot use Gopher literal (type Gopher) as type People in assignment: // Gopher does not implement People (Say method has pointer receiver) // func (g *Gopher) Say(s string) { // fmt.Println(s) // } func (g Gopher) Say(s string) { fmt.Println(s) } type PHPer struct{} func (P PHPer) Say(s string) { fmt.Println(s) } // main.go func pointer_vs_value_receiver() { var p People p = Gopher{} // 注释1 p.Say("go go go!") p = &PHPer{} // 故意用指针测试,结果ok p.Say("php!") }

如果将func (g Gopher) Say(s string)改为:func (g *Gopher) Say(s string), 则会报错:

./main.go:30:4: cannot use Gopher literal (type Gopher) as type People in assignment: Gopher does not implement People (Say method has pointer receiver)

大意是说:不能用Gopher的字面量给People赋值: Gopher没有实现People(Say方法使用的指针接收者)。 怎么用了指针就不行了??? 当然,上面的说法有一个简单的解释:接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响(因为是值拷贝)。

何时使用

何时使用指针接收者?

确实需要修改接收者的成员(如上面的Animal示例子中的Name就应该改为指针接收者)大型结构体想要避免拷贝,提高效率时需要修改接收者本身(如*int)…

何时用值接收者/何时不应该用指针接收者?

对于slice, map, channel等引用类型的值,拷贝本身也是拷贝他们的header信息, 如:type SliceHeader struct { Data uintptr Len int Cap int } 所以用指针就没什么必要了…

总之: 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

iface 和 eface 的区别是什么

iface 和 eface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{}

看下源码:

// 以下代码位于: $GOROOT/src/runtime/runtime2.go type iface struct { tab *itab data unsafe.Pointer } // layout of Itab known to compilers // allocated in non-garbage-collected memory // Needs to be in sync with // ../cmd/compile/internal/gc/reflect.go:/^func.dumptabs. 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,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的(TODO: 未考证)。

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

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

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

这里通过一张图来看下 iface 结构体的全貌: 接着来看一下 eface 的源码:

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

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

我们最后再来看下 _type 结构体:

// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize, // ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and // ../reflect/type.go:/^type.rtype. // ../internal/reflectlite/type.go:/^type.rtype. type _type struct { size uintptr // 类型大小 ptrdata uintptr // size of memory prefix holding all pointers hash uint32 // 类型的 hash 值 tflag tflag // 类型的 flag,和反射相关 // 内存对齐相关 align uint8 fieldAlign uint8 // 类型的编号,有bool, slice, struct 等等等等 kind uint8 // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool // gc 相关 // 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 *byte str nameOff ptrToThis typeOff }

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

// $GOROOT/src/runtime/type.go type slicetype struct { typ _type elem *_type } type functype struct { typ _type inCount uint16 outCount uint16 } type ptrtype struct { typ _type elem *_type } type structfield struct { name name typ *_type offsetAnon uintptr }

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

接口的动态类型和动态值

有了上面的知识, 我们很快就可以解决开始的问题了。

回答开始的问题

iface包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型和动态值。而接口值包括动态类型和动态值。

接口值的零值是指动态类型和动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil。 再看本文开头的代码:

func basic() { var i interface{} var r = func() *int { return nil }() i = r fmt.Println(i == nil, r == nil) }

i中的动态值data为nil, 但是动态类型tab不为nil(此时为*int), 所以i == nil是false

如何打印出接口的动态类型和值?

package main type iface struct { itab, data uintptr } func get_dynamic_type_and_value() { var a interface{} = nil var b interface{} = (*int)(nil) var x int = 5 var c interface{} = (*int)(&x) ia := *(*iface)(unsafe.Pointer(&a)) ib := *(*iface)(unsafe.Pointer(&b)) ic := *(*iface)(unsafe.Pointer(&c)) // {0 0} {4851360 0} {4851360 824634175152} // true fmt.Println(ia, ib, ic) fmt.Println(*(*int)(unsafe.Pointer(ic.data)) == x) }

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

运行结果如下:

{0 0} {4851360 0} {4851360 824634175152} true

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

类型断言和类型转换

类型断言: 是不是某种类型 (典型的: _, ok := v.(int), v必须是interface类型), 断言是对接口进行的操作, 类型转换: 转换成某种类型 (典型的:i := int(v))

func type_assert_vs_conv() { var v interface{} = 100 i := v.(int) // 100 fmt.Println(i) // 运行时: // panic: interface conversion: interface {} is int, not map[string]string // m := v.(map[string]string) // fmt.Println(m) // 安全类型断言 // false _, ok := v.(map[string]string) fmt.Println(ok) // int switch v.(type) { case int: fmt.Println("int") default: fmt.Println("default") } // 类型转换必须是兼容的类型才可以 // ./main.go:91:9: cannot convert v (type interface {}) to type int: need type assertion // i = int(v) // 将int型的i转为float64型 // 100 var f float64 = float64(i) fmt.Println(f) }

一些常见问题

String()方法

看代码:

package main import "fmt" type App struct { Name string } func (a App) String() string { return fmt.Sprintf("app: [%s]", a.Name) } func string_method() { a := App{Name: "awesome app a"} fmt.Println(a) b := &App{Name: "awesome app b"} fmt.Println(b) }

上面的输出为:

app: [awesome app a] app: [awesome app b]

如果将String()方法改为:

func (a *App) String() string { return fmt.Sprintf("app: [%s]", a.Name) }

那么结果为:

{awesome app a} app: [awesome app b]

其实还是之前的值接收者我指针接收者的区别, 不妨再回头看看。

参考

https://qcrao91.gitbook.io/go/interfacehttps://mp.weixin.qq.com/s/EbxkBokYBajkCR-MazL0ZA

(完)

最新回复(0)