有趣实验: hack go interface
source link: https://colobu.com/2020/01/19/hack-go-interface/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Go interface代表一组方法的集合,凡是实现这组集合的对象都称之为实现了这个接口,具体的对象不必像其它编程语言比如Java那样必须显示的 Implement
某个或者某些接口,所以说Go的接口类型是鸭子类型( Duck type
)。
“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
- 鸭子测试
当然, 对于一个Go基本入门的开发者来说,这些概念早就深入人心,那么Go是如何实现接口和具体类型的转换的呢?
比如下面的一段代码,定义了一个接口 Op
以及一个具体实现 Adder
。 Op
接口包含一个 Add
方法,而 Adder
具体实现了这个方法,因此我们称 Adder
实现了 Op
接口。
type Op interface{ Add(a, b int32) int32 } type Adder struct{} //go:noinline func (adder Adder) Add(a, b int32) int32 { return a + b }
然后我们可以把 Adder
的实例可以赋值给 Op
类型的变量:
func main() { adder := Adder{} var op Op = adder op.Add(10,32) }
事实上,Go编译器在背后默默做了很多的工作,因为 op
的类型和 adder
的类型是不一样的,一个是 interface
一个是 struct
,它俩的数据布局都不是一样的,编译器在 编译 的时候做了转换。
编译器会为用到的接口和具体实现类型建立独一的关联对象 go.itab."main".Adder,"main".Op(SB)
。
在编译接口的方法调用的时候会将具体的类型实例地址和方法填充到此类型中,通过操作这个类型调用实例的方法。
现在网上已经有一些介绍Go 接口内幕的文章,目前我觉得介绍最深入最详细的是 o-internals ch2 interfaces , 也被翻译成了中文: Go语言内幕第二章 接口 ,我觉得如果你仔细阅读了这篇文章,应该能够清楚了了解Go接口的具体实现了。
那么,进一步,我们可以通过hack的方法看看接口类型到底是个啥,纯粹是了解一下Go接口类型接口以及通过指针反解出方法来,当然这些hack方式并没有实践意义,纯粹为了好玩。
通用的接口类型是 runtime.iface
( runtime.eface
是特意为 interface{}
定义的,因为 interface{}
没有方法集,所以可以在 runtime.iface
简化),如果我们把它从 runtime
下摘出来,那么代码如下:
type iface struct { tab *itab data unsafe.Pointer } 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 } ......
定义了好多类型,我们把这些类型压缩在一个struct中,那么可以得到下面的接口类型的定义:
type iface2 struct { tab *struct { // 接口类型的元数据 inter *struct { typ struct { size uintptr ptrdata uintptr hash uint32 tflag uint8 align uint8 fieldAlign uint8 kind uint8 equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte str int32 ptrToThis int32 } pkgpath struct { bytes *byte } mhdr []struct { name nameOff ityp typeOff } } // 具体实例的类型的元数据 _type *struct { size uintptr ptrdata uintptr hash uint32 tflag uint8 align uint8 fieldAlign uint8 kind uint8 equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte str int32 ptrToThis int32 } hash uint32 _ [4]byte // 具体实例的方法 fun [1]uintptr } // 具体实例的地址 data unsafe.Pointer }
tab
是接口的元数据( inter
)、具体类型的元数据( _type
)、接口的虚函数列表( fun
),具体类型的地址( data
)等数据组成,这写类型Go并没有暴露出来,所以我们可以自己定义,并通过 unsafe
的方式把一个接口对象转换成我们定义的类型:
type addFun func(*Adder, int32, int32) int32 func testiface(adder *Adder, op Op) { var ifa = (*iface2)(unsafe.Pointer(&op)) fmt.Printf("%+v\n", ifa) }
得到了 ifa
对象你可以尝试输出它的一些字段看看,看看是否是期望的结果。
注意到 ifa.tab.fun
是接口的虚函数列表,这里的:chestnut:中使用 adder.Add
填充,本例中只有一个函数。那么我们可以想办法从 ifa.tab.fun
得到函数对象,并调用它。
ifa.tab.fun[0]
得到这个函数的地址,使用 runtime.FuncForPC
可以得到它的名称和方法体。
func testiface(adder *Adder, op Op) { var ifa = (*iface2)(unsafe.Pointer(&op)) f := runtime.FuncForPC(ifa.tab.fun[0]) fmt.Printf("adder.Add name: %s\n", f.Name()) }
可以看到可以正确的输出函数的名称。
下一步我们构造出函数来,然后调用它。首先我们定义一个函数类型,方法也是一种函数,只不过方法的Receiver作为函数的第一个参数:
type addFun func(*Adder, int32, int32) int32
然后通过反射,基于方法体创建出一个函数出来,并赋值给一个变量。
type Func struct { codePtr uintptr } func createFuncForCodePtr(outFuncPtr interface{}, entry uintptr) { outFuncVal := reflect.ValueOf(outFuncPtr).Elem() newFuncVal := reflect.MakeFunc(outFuncVal.Type(), nil) funcValuePtr := reflect.ValueOf(newFuncVal).FieldByName("ptr").Pointer() funcPtr := (*Func)(unsafe.Pointer(funcValuePtr)) funcPtr.codePtr = entry outFuncVal.Set(newFuncVal) }
下一步就是调用这个生生造出来的函数了:
type addFun func(*Adder, int32, int32) int32 func testiface(adder *Adder, op Op) { var ifa = (*iface2)(unsafe.Pointer(&op)) f := runtime.FuncForPC(ifa.tab.fun[0]) fmt.Printf("adder.Add name: %s\n", f.Name()) var fn addFun createFuncForCodePtr(&fn, f.Entry()) v := fn(adder,10,32) fmt.Printf("calculated result: %d\n", v) } type Func struct { codePtr uintptr } func createFuncForCodePtr(outFuncPtr interface{}, entry uintptr) { outFuncVal := reflect.ValueOf(outFuncPtr).Elem() newFuncVal := reflect.MakeFunc(outFuncVal.Type(), nil) funcValuePtr := reflect.ValueOf(newFuncVal).FieldByName("ptr").Pointer() funcPtr := (*Func)(unsafe.Pointer(funcValuePtr)) funcPtr.codePtr = entry outFuncVal.Set(newFuncVal) }
虽然无聊的绕来绕去的演示了将接口对象转换成一个 interface struct
,并调用它的虚函数,但是通过这个演示,我们可以比较深刻的了解go接口的具体实现,以及go里面的一些小trick。
相关代码可以在 interfaces 找到。
Recommend
-
118
在十一月份的前端技术列表中,我们整合了一些令人感到惊叹的 GitHub 项目,其中包含了新的 CSS 框架、node.js包管理器,以及用于实现图标、加载效果、工具提示的纯 CSS 解决方案。 那么,让我们一起来看看吧。Have Fun ! 1.
-
106
Google超有趣小实验合集 | 不论你会不会编程,你都可以在这感受到AI的乐趣(园长诚意推荐)园长
-
86
沸点2.0更新,“有趣”又“有料” 2017年11月09日 03:24 · 阅读 3117
-
34
一项有趣的实验:装了杀软的主机真的安全吗?
-
21
有趣实验: hack go interface Go interface代表一组方法的集合,凡是实现这组集合的对象都称之为实现了这个接口,具体的对象不必...
-
9
SHIB(柴犬币),一场有趣的社区化实验 Odaily星球日报 原创 2021-05-08 06:50 热度 547371 分享 微信扫一扫:分享 ...
-
4
Site ColorhexText ColorAd ColorhexText Color
-
3
一个有趣的实验 —— 25号宇宙2022年4月13日 by anzhihe·0评论 · 401 人阅读 · 隐藏边栏...
-
0
.NET下数据库的负载均衡(有趣实验) 相...
-
1
.NET下数据库的负载均衡(有趣实验)(续)
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK