gomonkey用户如何对泛型打桩

问题的由来

gomonkey 社区,用户
ericuni 提了一个 issue,如下图所示:

issue-95.png

泛型是Go1.18引入的一个新特性,笔者在本地升级Go版本到Go1.18,复现了问题。
(1)产品代码示例:

func Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

(2)测试代码示例:

func TestGenericFuncPatch(t *testing.T) {
    Convey("TestGenericFuncPatch", t, func() {
        So(Add[int](1, 2), ShouldEqual, 3)
        So(Add(1, 2), ShouldEqual, 3)
        patches := ApplyFuncReturn(Add[int], 5)
        defer patches.Reset()
        So(Add(1, 2), ShouldEqual, 5)
    })
}

(3)执行该测试用例,复现问题,打桩未生效
测试用例执行结果:

=== RUN   TestGenericFuncPatch

  TestGenericFuncPatch ✔✔✘


Failures:

  * /Users/zhangxiaolong/Desktop/D/go-workspace/gostudy/framework-study/goconvey-study/slice/slice_test.go
  Line 203:
  Expected: '5'
  Actual:   '3'
  (Should be equal)


3 total assertions

--- FAIL: TestGenericFuncPatch (0.00s)
FAIL
FAIL    goconvey-study/slice    0.010s

说明:

  • Add[int](1, 2)Add(1, 2)两种写法等价,但Add(1, 2)这种写法更方便,因为这种写法使用了 Go 泛型类型实参的自动推导,从而不用显式的手动指定类型实参,实际上是编译器帮我们完成了相关工作;
  • 通过 gomonkey 的ApplyFuncReturn接口完成对函数Add[int]的打桩后,再次调用Add(1, 2),发现返回值仍然是3而不是期望的5,说明对该函数的打桩并未生效

解决方案

每次产品代码中实例化泛型函数的过程,比如Add[int],对应的汇编代码是一个函数调用,所以对实例化后的泛型函数打桩就变成了对函数调用打桩了。然而,每次产品代码中实例化泛型函数的时候,对应的汇编代码中的函数调用是不同的函数名字,比如func1, func2, ...等,这会导致虽然打桩当时成功了,但后续再实例化泛型函数的时候,对应的函数调用名称就变成了其他的,所以打桩没有真正生效,比如打桩打在了func2上,而之后的调用却发生在func3, func4上了。

其实,实例化泛型函数过程中产生的func1, func2, ...等都是中间函数,真正的泛型函数实例会在中间函数层被最终调用。不清楚Go语言为啥要这样设计泛型的实现,但它肯定有深层次的原因。我们只能从中间函数的代码段逐条解析,找到CALL指令后提取参数来计算泛型函数实例化后的真正地址。

找到泛型函数实例的真正地址后,就确定了真正的目标函数,但桩函数如何写?

泛型函数会占用AX、BXCX 三个寄存器来保存参数,而普通函数仅占用AXBX两个寄存器来来保存参数,同时泛型函数的第一个入参并不在AX中,而普通函数的第一个入参却在AX中,所以桩函数必须和泛型函数长的一模一样才行。

还有一点,空接口通过反射无法区分是泛型函数还是普通函数,所以泛型函数打桩接口中要通过bool值来区分到底是泛型函数还是普通函数,这就导致接口不同。

综上,泛型函数打桩接口与普通函数不同,同时泛型函数需要特殊获取目标函数的地址,到了函数跳转时刻,泛型函数的桩函数使用的寄存器个数与普通函数的桩函数不同,从而汇编代码不同,几乎是两套代码。

考虑到泛型的使用场景并不广泛,同时也有替代方案(比如接口+反射),而要完整的支持泛型打桩却需要为 gomonkey 引入很高的复杂度,因此不是很划算,并且 Go 语言泛型机制也在不断的演进,或许后面再支持泛型打桩就变得很容易了。

于是我们计划在 gomonkey 框架层面暂时不支持对泛型打桩,但是 gomonkey 用户或多或少还是会有对泛型打桩的诉求,那么这种情况下应该怎么办是一个值得思考的问题。

如果让中间函数变成唯一,那么对中间函数打桩就是对泛型打桩。这时,我们想到了函数变量,可以规避直接对泛型打桩带来的一系列问题。

典型案例

我们引入函数变量,将上述复现问题的案例打造成泛型函数打桩的典型案例,供大家参考!

(1)产品代码中增加函数变量的定义

func Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

// 函数变量的定义
var AddInt = Add[int]

(2)在测试代码中对函数变量进行打桩

func TestGenericFuncPatch(t *testing.T) {
    Convey("TestGenericFuncPatch", t, func() {
        So(AddInt(1, 2), ShouldEqual, 3)
        // 也可以使用 ApplyFuncVar 接口,但此处使用 ApplyFuncVarReturn 接口更简洁
        patches := ApplyFuncVarReturn(&AddInt, 5)
        defer patches.Reset()
        So(AddInt(1, 2), ShouldEqual, 5)
    })

}

(3)执行该测试用例,结果符合预期
测试用例执行结果:

=== RUN   TestGenericFuncPatch

  TestGenericFuncPatch ✔✔


2 total assertions

--- PASS: TestGenericFuncPatch (0.00s)

案例延伸

gomonkey 既然对泛型函数可以打桩了,那么对泛型方法是否也可以打桩呢?

很不幸,目前 Go 的方法并不支持泛型,如下:

type A struct {
}

// 不支持泛型方法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

但是因为receiver支持泛型, 所以如果想在方法中使用泛型的话,目前唯一的办法就是曲线救国,迂回地通过receiver使用类型形参:

type A[T int | float32 | float64] struct {
}

// 方法可以使用类型定义中的形参 T 
func (receiver A[T]) Add(a T, b T) T {
    return a + b
}

// 用法:
var a A[int]
a.Add(1, 2)

var aa A[float32]
aa.Add(1.0, 2.0)

打桩过程与之前泛型函数类似,供大家参考!

(1)产品代码中还是增加函数变量的定义

type A[T int | float32 | float64] struct {
}

// 方法可以使用类型定义中的形参 T 
func (receiver A[T]) Add(a T, b T) T {
    return a + b
}

// 函数变量的定义
var AObject = new(A[int])
var SAddInt = AObject.Add

(2)在测试代码中对函数变量进行打桩

func TestGenericMethodPatch(t *testing.T) {
    Convey("TestGenericMethodPatch", t, func() {
        So(SAddInt(1, 2), ShouldEqual, 3)
        patches := ApplyFuncVarReturn(&SAddInt, 5)
        defer patches.Reset()
        So(SAddInt(1, 2), ShouldEqual, 5)
    })
}

(3)执行该测试用例,结果符合预期
测试用例执行结果:

=== RUN   TestGenericMethodPatch

  TestGenericMethodPatch ✔✔


2 total assertions

--- PASS: TestGenericMethodPatch (0.00s)

小结

用户期望可以在框架层面解决一切共性问题,这本属于合理的诉求,但现实有时很骨感,在种种约束下必须改变思路,而在用户侧较优雅的解决相关问题也是一种不错的选择。

本文针对 gomonkey 用户,给出了如何对泛型进行打桩的解决方案,并提供了典型案例及其延伸,希望对读者有一定的帮助!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 121,077评论 1 245
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 52,864评论 1 205
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 76,245评论 0 170
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 36,967评论 0 130
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 43,809评论 1 208
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 36,298评论 1 128
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 28,236评论 2 210
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 27,340评论 0 121
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 26,333评论 5 177
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 30,444评论 0 181
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 27,597评论 1 172
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 28,843评论 1 181
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 23,106评论 0 25
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 25,728评论 2 166
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 29,649评论 3 176
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 24,574评论 0 4
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 24,553评论 0 115
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 30,922评论 2 194
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 31,286评论 2 190

推荐阅读更多精彩内容