美文网首页
gomonkey用户如何对泛型打桩

gomonkey用户如何对泛型打桩

作者: _张晓龙_ | 来源:发表于2022-08-07 23:58 被阅读0次

    问题的由来

    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 用户,给出了如何对泛型进行打桩的解决方案,并提供了典型案例及其延伸,希望对读者有一定的帮助!

    相关文章

      网友评论

          本文标题:gomonkey用户如何对泛型打桩

          本文链接:https://www.haomeiwen.com/subject/qvjiwrtx.html