深入 go interface 底层原理
打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!
原文链接:深入 go interface 底层原理 欢迎点赞关注
什么是 interface
在 Go 语言中,interface(接口)是一种抽象的类型定义。它主要用于定义一组方法的签名,但不包含方法的实现细节。接口提供了一种规范,任何类型只要实现了接口中定义的所有方法,就被认为是实现了该接口。这使得不同的类型可以以一种统一的方式被处理,增强了代码的灵活性、可扩展性和可维护性。
例如,如果定义一个 Shape 接口,包含一个 Area 方法:
type Shape interface {Area() float64
}// 圆形结构体
type Circle struct {Radius float64
}// 圆形实现面积计算方法
func (c Circle) Area() float64 {return 3.14 * c.Radius * c.Radius
}// 矩形结构体
type Rectangle struct {Length, Width float64
}// 矩形实现面积计算方法
func (r Rectangle) Area() float64 {return r.Length * r.Width
}// 打印形状面积的函数
func printArea(s Shape) {fmt.Printf("面积: %.2f\n", s.Area())
}func main() {circle := Circle{Radius: 5}rectangle := Rectangle{Length: 4, Width: 6}printArea(circle)printArea(rectangle)
}
那么无论是 Circle 结构体还是 Rectangle 结构体,只要它们都实现了 Area 方法,就都可以被当作 Shape 类型来使用。
接口在 Go 语言中广泛应用于解耦代码、实现多态性、定义通用的行为规范等场景。它让代码更加模块化和易于管理,有助于提高代码的质量和可复用性。
底层数据结构
在 Go 语言中,有两种“interface”,一种是空接口(`interface{}`),它可以存储任意类型的值;另一种是非空接口,这种接口明确地定义了一组方法签名,只有实现了这些方法的类型才能被认为是实现了该非空接口。 下面讨论一下这两种接口的底层实现。
空接口与非空接口
在 Go 语言中,空接口的底层数据结构是 runtime.eface :
type eface struct {_type *_typedata unsafe.Pointer
}
_type 字段指向一个 _type 结构体,该结构体包含了所存储值的详细类型信息,data 字段则是一个 unsafe.Pointer ,它直接指向实际存储的数据的内存地址。
非空接口的底层数据结构是 runtime.iface:
type iface struct {tab *itabdata unsafe.Pointer
}
同样,data 字段也是一个指向实际数据的指针。然而,这里的重点是 tab 字段,它指向一个 itab 结构体。
itab 结构体
itab 结构体包含接口的类型信息和指向数据的类型信息:
type itab struct {inter *interfacetype_type *_typehash uint32 // copy of _type.hash. Used for type switches._ [4]bytefun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}type interfacetype struct {typ _type // 接口的类型pkgpath name // 接口所在路径mhdr []imethod // 接口所定义的方法列表
}
-
inter 字段描述了接口自身的类型信息,包括接口所定义的方法等。
-
_type 字段存储了实际值的类型信息。
-
hash 字段是对 _type 结构体中哈希值的拷贝,它在进行类型比较和转换等操作时能够提供快速的判断依据。
-
fun 字段则是一个动态大小的函数指针数组,当fun[0]=0时,表示_type并没有实现该接口(这里指的是itab下的_type),当实现了接口时,fun存放了第一个接口方法的地址,其他方法依次往后存放。在这里fun存储的其实是接口方法对应的实际类型的方法,每次调用发方法时实行动态分派。
_type 结构体
_type是runtime对Go任意类型的内部表示。
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
}
-
size描述类型的大小
-
hash数据的hash值
-
align指对齐
-
fieldAlgin是这个数据嵌入结构体时的对齐
-
kind是一个枚举值,每种类型对应了一个编号
-
alg是一个函数指针的数组,存储了hash/equal这两个函数操作。
-
gcdata存储了垃圾回收的GC类型的数据,精确的垃圾回收中,就是依赖于这里的gcdata
-
nameOff和typeOff为int32,表示类型名称和类型的指针偏移量,这两个值会在运行期间由链接器加载到runtime.moduledata结构体中,通过以下两个函数可以获取偏移量
func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}
若t为_type类型,那么调用resolveTypeOff(t,t.ptrToThis)可以获得t的一份拷贝t’。
Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段来进行管理的:
type arraytype struct {typ _typeelem *_typeslice *_typelen uintptr
}type chantype struct {typ _typeelem *_typedir uintptr
}type slicetype struct {typ _typeelem *_type
}type structtype struct {typ _typepkgPath namefields []structfield
}
这些数据类型的结构体定义,是反射实现的基础。
类型转换
接口转接口
path:/usr/local/go/src/runtime/iface.go
func convI2I(inter *interfacetype, i iface) (r iface) {tab := i.tabif tab == nil {return}// 接口类型相同直接赋值if tab.inter == inter {r.tab = tabr.data = i.datareturn}// 否则生成新的itabr.tab = getitab(inter, tab._type, false)r.data = i.datareturn
}
数据类型转空接口
path:/usr/local/go/src/runtime/iface.go
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {if raceenabled {raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))}if msanenabled {msanread(elem, t.size)}// mallockgc一块新内存x := mallocgc(t.size, t, true)// TODO: We allocate a zeroed object only to overwrite it with actual data.// 值赋值进去typedmemmove(t, x, elem)// _type直接复制e._type = t// data指向新分配的内存e.data = xreturn
}
数据类型转非空接口
path:/usr/local/go/src/runtime/iface.go
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {t := tab._type// 如果开启了竞争检测,通过竞争检测读取数据if raceenabled {raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))}// 如果开启了 The memory sanitizer (msan)if msanenabled {msanread(elem, t.size)}// 重新分配内存x := mallocgc(t.size, t, true)// 对内存做优化typedmemmove(t, x, elem)i.tab = tabi.data = xreturn
}
可以看到从接口转接口是没有发生内存的重新分配的,如果类型相同直接进行了赋值;而从数据类型转接口重新分配了内存,这是因为Go里面接口转换时接口本身的数据不能被改变,所以接口可以使用同一块内存;而数据类型转换的接口可能数据发生改变,为了避免改变接口数据,所以重新分配了内存并拷贝原来的数据。这也是反射非指针变量时无法直接改变变量数据的原因,因为反射会先将变量转换为空接口类型。以上只是举例了itab相关的部分函数,所有类型转换相关的函数都在iface.go文件下。
类型断言
接口-接口断言
path:/usr/local/go/src/runtime/iface.go
func assertI2I(inter *interfacetype, i iface) (r iface) {tab := i.tabif tab == nil {// explicit conversions require non-nil interface value.panic(&TypeAssertionError{"", "", inter.typ.string(), ""})}if tab.inter == inter {r.tab = tabr.data = i.datareturn}r.tab = getitab(inter, tab._type, false)r.data = i.datareturn
}func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {tab := i.tabif tab == nil {return}if tab.inter != inter {tab = getitab(inter, tab._type, true)if tab == nil {return}}r.tab = tabr.data = i.datab = truereturn
}
有两种形式,一个返回值或两个返回值。一个返回值时,断言失败会panic(getitab失败会panic),两个则会返回是否断言成功(此时getitab的canfail参数传true,失败是会return nil,从而不会panic)。可以看到接口断言直接比较了inter,一样则直接赋值,否则调用getitab再做判断。
接口-类型断言
接口断言是否转换为具体类型,是编译器直接生成好的代码去做的。代码示例:
package main
import "fmt"
var EBread interface{}
var a int
var EVALUE = 666
func main() {EBread = EVALUEa = EBread.(int)fmt.Print(a)
}
执行go tool compile -S main.go >> main.S进行反汇编,查看汇编代码。将变量定义为全局变量,这样汇编代码中会以"".的方式引用变量,可以直接看到变量名。
由于汇编代码较长,这里只摘取主要部分做分析。
0x0058 00088 (main.go:9) MOVQ AX, "".EBread+8(SB) // AX存放EBread的数据值
0x005f 00095 (main.go:10) MOVQ "".EBread(SB), CX // CX=
0x0066 00102 (main.go:10) LEAQ type.int(SB), DX // DX=type.int的偏移地址
0x006d 00109 (main.go:10) CMPQ CX, DX // 比较CX和DX,即比较type.int与EBread的数据类型是否相同
0x0070 00112 (main.go:10) JNE 232 // 不等则跳转至232
0x0072 00114 (main.go:10) MOVQ (AX), AX // 解引用
0x0075 00117 (main.go:10) MOVQ AX, "".a(SB) // 赋值
可以看到empty interface转类型是编译器直接生成代码进行的对比,而非运行时调用函数进行动态的对比。
Interface 的陷阱
nil 判断
我们先看看下面例子:
func main() {var a *intvar b interface{}fmt.Println("a == nil", a == nil)fmt.Println("b == nil", b == nil)b = nil fmt.Println("b == nil", b == nil)b = a fmt.Println("b == nil", b == nil)
}$ go run main.go
a == nil true
b == nil true
b == nil true
b == nil false
对于接口判断 == nil 时,只有接口所指向的类型和值都为 nil 时接口才为 nil,如果想比较准确的判断接口类型是否是 nil 可以使用反射实现,但是有一定性能开销。
package mainimport ("fmt""reflect"
)func isInterfaceNil(i interface{}) bool {if i == nil {return true}value := reflect.ValueOf(i)switch value.Kind() {case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:return value.IsNil()default:return false}
}
func main() {var myInterface interface{}var myType *MyType = nilmyInterface = myTypefmt.Println(isInterfaceNil(myInterface))
}
值与指针接收者
先看下面几个例子:
情况一:使用指针作为接收者实现接口,使用结构体值类型调用接口方法,编译不通过。
type Duck interface {Quack()
}type Cat struct{}// 使用指针作为接收者实现接口
func (c *Cat) Quack() {fmt.Println("meow")
}func main() var c Duck = Cat{}// 使用结构体值类型调用方法c.Quack()
}$ go build interface.go
./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment:Cat does not implement Duck (Quack method has pointer receiver)
情况二:使用值类型作为接收者实现接口,使用结构体值类型调用接口方法,编译通过。
type Duck interface {Quack()
}type Cat struct{}// 使用值类型作为接收者实现接口
func (c Cat) Quack() {fmt.Println("meow")
}func main() var c Duck = Cat{}// 使用结构体值类型调用方法c.Quack()
}$ go run interface.go
meow
情况三:使用指针类型作为接收者实现接口,使用结构体指针类型调用方法,编译通过。
type Duck interface {Quack()
}type Cat struct{}// 使用指针类型作为接收者实现接口
func (c *Cat) Quack() {fmt.Println("meow")
}func main() var c Duck = &Cat{}// 使用结构体指针类型调用方法c.Quack()
}$ go run interface.go
meow
情况四:使用值类型作为接收者实现接口,使用结构体指针类型调用方法,编译通过。
type Duck interface {Quack()
}type Cat struct{}// 使用值类型作为接收者实现接口
func (c Cat) Quack() {fmt.Println("meow")
}func main() var c Duck = &Cat{}// 使用结构体指针类型调用方法c.Quack()
}$ go run interface.go
meow
使用结构体指针类型调用方法 | 使用结构体值类型调用接口方法 | |
---|---|---|
使用指针作为接收者实现接口 | 通过 | 不通过 |
使用值类型作为接收者实现接口 | 通过 | 通过 |
Interface 的应用
依赖倒置
依赖倒置(Dependency Inversion Principle,DIP) 是软件设计中的一个重要原则。
其核心思想是:
-
高层模块不应该依赖于低层模块,二者都应该依赖于抽象(接口或抽象类)。
-
抽象不应该依赖于细节,细节应该依赖于抽象。
通过遵循依赖倒置原则,可以带来以下好处:
-
提高代码的灵活性和可维护性:当低层模块的实现发生变化时,高层模块不需要进行大量的修改,只需要更改依赖的抽象的实现即可。
-
促进模块之间的解耦:使得各个模块之间的依赖关系更加清晰和松散,降低了模块之间的耦合度。
下面以数据库调用为例:
package mainimport "fmt"// 定义数据库操作接口
type DBOperation interface {QueryData(key string) string
}// 实现 MySQL 数据库操作
type MySQLDB struct{}func (m MySQLDB) QueryData(key string) string {return fmt.Sprintf("Querying from MySQL with key: %s", key)
}// 实现 PostgreSQL 数据库操作
type PostgreSQLDB struct{}func (p PostgreSQLDB) QueryData(key string) string {return fmt.Sprintf("Querying from PostgreSQL with key: %s", key)
}// 业务服务结构体,依赖于数据库操作接口
type BusinessService struct {db DBOperation
}// 业务方法
func (b BusinessService) ProcessData(key string) {result := b.db.QueryData(key)fmt.Println(result)
}func main() {// 使用 MySQL 数据库mysql := MySQLDB{}serviceWithMySQL := BusinessService{mysql}serviceWithMySQL.ProcessData("user1")// 使用 PostgreSQL 数据库postgres := PostgreSQLDB{}serviceWithPostgres := BusinessService{postgres}serviceWithPostgres.ProcessData("user2")
}
在上述示例中:
- 定义了 DBOperation 接口,包含 QueryData 方法。
- 有 MySQLDB 和 PostgreSQLDB 两个不同的数据库实现了该接口。
- BusinessService 结构体依赖于 DBOperation 接口,而不是具体的数据库实现。
这样,BusinessService 的代码不与具体的数据库实现紧密耦合,实现了依赖倒置。当需要切换数据库时,只需要注入不同的数据库实现对象,无需修改 BusinessService 的核心逻辑。
比如,后续如果要支持新的数据库,如 MongoDB,只需要创建新的结构体实现 DBOperation 接口,并在使用处注入即可。
策略模式
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。以支付为例,用户可以选择微信或者支付宝进行支付:
// 定义接口
type Payer interface {CreateOrder()PayRpc()UpdateOrder()
}// 支付宝支付实现
type Alipay struct {}
func (a *Alipay)CreateOrder(){// ...
}
func (a *Alipay)PayRpc(){// ...
}
func (a *Alipay)UpdateOrder(){// ...
}// 微信支付实现
type Wxpay struct {}
func (w *Wxpay)CreateOrder(){
// ...
}
func (w *Wxpay)PayRpc(){
// ...
}
func (w *Wxpay)UpdateOrder(){/
/ ...
}// 工厂+策略模式
func NewPayer(PayType string) Payer {switch PayType {case "alipay":return &Alipay{}case "weixin":return &Wxpay{}// case "other":// retrun &OtherPay{}}
}func Pay(arg) {payer := NewPayer(arg.type)payer.CreateOrder()payer.PayRpc()payer.UpdateOrder()
}
实现多态
多态是一种运行期的行为,它有以下几个特点:
-
一种类型具有多种类型的能力
-
允许不同的对象对同一消息做出灵活的反应
-
以一种通用的方式对待个使用的对象
-
非动态语言必须通过继承和接口的方式来实现
package mainimport "fmt"// 定义形状接口
type Shape interface {Area() float64
}// 圆形结构体
type Circle struct {Radius float64
}// 圆形实现面积计算方法
func (c Circle) Area() float64 {return 3.14 * c.Radius * c.Radius
}// 矩形结构体
type Rectangle struct {Length, Width float64
}// 矩形实现面积计算方法
func (r Rectangle) Area() float64 {return r.Length * r.Width
}// 打印形状面积的函数
func printArea(s Shape) {fmt.Printf("面积: %.2f\n", s.Area())
}func main() {circle := Circle{Radius: 5}rectangle := Rectangle{Length: 4, Width: 6}printArea(circle)printArea(rectangle)
}
参考
https://mp.weixin.qq.com/s/Wadii1L9-fg6bJBfYJV0mQ
https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/
https://qcrao91.gitbook.io/go/interface/
本文由mdnice多平台发布