打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!
原文链接:深入 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 *_type
data unsafe.Pointer
}
_type 字段指向一个 _type 结构体,该结构体包含了所存储值的详细类型信息,data 字段则是一个 unsafe.Pointer ,它直接指向实际存储的数据的内存地址。
非空接口的底层数据结构是 runtime.iface:
type iface struct {
tab *itab
data unsafe.Pointer
}
同样,data 字段也是一个指向实际数据的指针。然而,这里的重点是 tab 字段,它指向一个 itab 结构体。
itab 结构体
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.
}
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 uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind 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 *byte
str nameOff
ptrToThis 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 _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
}
这些数据类型的结构体定义,是反射实现的基础。
类型转换
接口转接口
path:/usr/local/go/src/runtime/iface.go
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
}
// 否则生成新的itab
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}
数据类型转空接口
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 = x
return
}
数据类型转非空接口
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 = tab
i.data = x
return
}
可以看到从接口转接口是没有发生内存的重新分配的,如果类型相同直接进行了赋值;而从数据类型转接口重新分配了内存,这是因为Go里面接口转换时接口本身的数据不能被改变,所以接口可以使用同一块内存;而数据类型转换的接口可能数据发生改变,为了避免改变接口数据,所以重新分配了内存并拷贝原来的数据。这也是反射非指针变量时无法直接改变变量数据的原因,因为反射会先将变量转换为空接口类型。以上只是举例了itab相关的部分函数,所有类型转换相关的函数都在iface.go文件下。
类型断言
接口-接口断言
path:/usr/local/go/src/runtime/iface.go
func assertI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {
// explicit conversions require non-nil interface value.
panic(&TypeAssertionError{"", "", inter.typ.string(), ""})
}
if tab.inter == inter {
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
tab := i.tab
if tab == nil {
return
}
if tab.inter != inter {
tab = getitab(inter, tab._type, true)
if tab == nil {
return
}
}
r.tab = tab
r.data = i.data
b = true
return
}
有两种形式,一个返回值或两个返回值。一个返回值时,断言失败会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 = EVALUE
a = 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 *int
var 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 main
import (
"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 = nil
myInterface = myType
fmt.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 main
import "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 main
import "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多平台发布