当前位置: 首页 > news >正文

深入 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 // 接口所定义的方法列表
}
  1. inter 字段描述了接口自身的类型信息,包括接口所定义的方法等。

  2. _type 字段存储了实际值的类型信息。

  3. hash 字段是对 _type 结构体中哈希值的拷贝,它在进行类型比较和转换等操作时能够提供快速的判断依据。

  4. 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) 是软件设计中的一个重要原则。

其核心思想是:

  1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象(接口或抽象类)。

  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

通过遵循依赖倒置原则,可以带来以下好处:

  1. 提高代码的灵活性和可维护性:当低层模块的实现发生变化时,高层模块不需要进行大量的修改,只需要更改依赖的抽象的实现即可。

  2. 促进模块之间的解耦:使得各个模块之间的依赖关系更加清晰和松散,降低了模块之间的耦合度。

下面以数据库调用为例:

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()
}

实现多态

多态是一种运行期的行为,它有以下几个特点:

  1. 一种类型具有多种类型的能力

  2. 允许不同的对象对同一消息做出灵活的反应

  3. 以一种通用的方式对待个使用的对象

  4. 非动态语言必须通过继承和接口的方式来实现

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多平台发布

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 多模态模型BLIP2学习笔记
  • apache2和httpd web服务器
  • JavaScript 和 HTML5 Canvas实现图像绘制与处理
  • Java:多线程(进程线程、线程状态、创建线程、线程操作)
  • 【 问题 】 AT32 F413CB 设置SRAM大小为64KB 导致Flash后64KB代码执行变慢 解决办法
  • 搞懂数据结构与Java实现
  • 维度的精减:sklearn中分层特征降维技术全解析
  • 如何对同一个项目,不同分支,开两个IDEA窗口?
  • 单例模式及其思想
  • 【刷题汇总 -- 游游的重组偶数、体操队形、二叉树中的最大路径和】
  • AI智驾时代降临,端到端奏响“三重奏”
  • Hive-内部表和外部表
  • 【网络】UDP协议——传输层、端口号、UDP协议、UDP协议端格式、UDP的特点、UDP的缓冲区、UDP使用注意事项
  • LeeCode Practice Journal | Day30_GA04
  • Notepad++ 安装 compare 插件
  • 【跃迁之路】【444天】程序员高效学习方法论探索系列(实验阶段201-2018.04.25)...
  • Docker: 容器互访的三种方式
  • Javascript基础之Array数组API
  • Meteor的表单提交:Form
  • passportjs 源码分析
  • Python学习之路16-使用API
  • Vue.js-Day01
  • 百度小程序遇到的问题
  • 机器学习 vs. 深度学习
  • 聚类分析——Kmeans
  • 目录与文件属性:编写ls
  • 前端技术周刊 2019-02-11 Serverless
  • 微信小程序实战练习(仿五洲到家微信版)
  • 为视图添加丝滑的水波纹
  • 用jQuery怎么做到前后端分离
  • 优秀架构师必须掌握的架构思维
  • 你对linux中grep命令知道多少?
  • UI设计初学者应该如何入门?
  • ​520就是要宠粉,你的心头书我买单
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • ​LeetCode解法汇总518. 零钱兑换 II
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • ‌移动管家手机智能控制汽车系统
  • # AI产品经理的自我修养:既懂用户,更懂技术!
  • #70结构体案例1(导师,学生,成绩)
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (152)时序收敛--->(02)时序收敛二
  • (2021|NIPS,扩散,无条件分数估计,条件分数估计)无分类器引导扩散
  • (4) openssl rsa/pkey(查看私钥、从私钥中提取公钥、查看公钥)
  • (AtCoder Beginner Contest 340) -- F - S = 1 -- 题解
  • (附源码)ssm航空客运订票系统 毕业设计 141612
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (贪心 + 双指针) LeetCode 455. 分发饼干
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (循环依赖问题)学习spring的第九天
  • (一)eclipse Dynamic web project 工程目录以及文件路径问题
  • .Net Core 微服务之Consul(三)-KV存储分布式锁
  • .Net Core和.Net Standard直观理解
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)