【问题标题】:How are functions implemented?功能是如何实现的?
【发布时间】:2018-05-23 22:02:49
【问题描述】:

我一直在玩reflect 包,我注意到函数的功能有多么有限。

package main

import (
    "fmt"
    "reflect"
    "strings"
)

func main() {
    v := reflect.ValueOf(strings.ToUpper)
    fmt.Printf("Address: %v\n", v)               // 0xd54a0
    fmt.Printf("Can set? %d\n", v.CanSet())      // False
    fmt.Printf("Can address? %d\n", v.CanAddr()) // False
    fmt.Printf("Element? %d\n", v.Elem())        // Panics
}

游乐场链接here

我被告知函数是带有一组指令的内存地址(因此v 打印出0xd54a0),但看起来我无法获得该内存地址的地址,设置它,或取消引用它。

那么,Go 函数是如何在底层实现的?最终,我希望通过使函数指向我自己的代码来操纵 strings.ToUpper 函数。

【问题讨论】:

    标签: function pointers go


    【解决方案1】:

    免责声明:

    1. 我最近才开始深入研究 golang 编译器,更具体地说:go 汇编器及其映射。因为我绝不是专家,所以我不会尝试在这里解释所有细节(因为我的知识很可能仍然缺乏)。我将在底部提供几个链接,可能值得查看以了解更多详细信息。

    2. 你试图做的事情对我来说意义不大。如果在运行时,您试图修改一个函数,那么您之前可能做错了什么。这只是以防你想弄乱任何功能。您尝试使用 strings 包中的函数做某事的事实使这更加令人担忧。 reflect 包允许您编写非常通用的函数(例如,具有请求处理程序的服务,但您想将任意参数传递给这些处理程序需要您有一个处理程序,处理原始请求,然后调用相应的处理程序。您不可能知道那个处理程序是什么样的,所以你使用反射来计算出所需的参数......)。


    现在,函数是如何实现的?

    Go 编译器是一个复杂的代码库,但值得庆幸的是,语言设计及其实现已经被公开讨论过。据我所知,golang 函数的编译方式基本上与 C 语言中的函数几乎相同。但是,调用函数有点不同。 Go 函数是一流的对象,这就是为什么您可以将它们作为参数传递、声明函数类型以及为什么 reflect 包必须允许您对函数参数使用反射。

    本质上,函数不是直接处理的。函数通过函数“指针” 传递和调用。函数实际上是类似于mapslice 的类型。它们持有指向实际代码和调用数据的指针。简单来说,将函数视为一种类型(在伪代码中):

    type SomeFunc struct {
        actualFunc *func(...) // pointer to actual function body
        data struct {
            args []interface{} // arguments
            rVal []interface{} // returns
            // any other info
        }
    }
    

    这意味着reflect 包可用于例如计算函数期望的参数数量和返回值。它还告诉您返回值是什么。整个函数“类型”将能够告诉您函数所在的位置,以及它期望和返回的参数,但仅此而已。 IMO,这就是你真正需要的。

    由于这种实现,您可以创建具有如下函数类型的字段或变量:

    var callback func(string) string
    

    这将创建一个基础值,根据上面的伪代码,它看起来像这样:

    callback := struct{
        actualFunc: nil, // no actual code to point to, function is nil
        data: struct{
           args: []interface{}{string}, // where string is a value representing the actual string type
            rVal: []interface{}{string},
        },
    }
    

    然后,通过分配与argsrVal 约束匹配的任何函数,您可以确定callback 变量指向的可执行代码:

    callback = strings.ToUpper
    callback = func(a string) string {
        return fmt.Sprintf("a = %s", a)
    }
    callback = myOwnToUpper
    

    我希望这能解决 1 或 2 件事,但如果没有,这里有一堆链接可能会更清楚地说明这个问题。


    更新

    当您尝试更换您正在使用的用于测试目的的函数时,我建议您不要使用反射,而是注入模拟函数,这是 WRT 更常见的做法测试开始。更不用说它变得容易多了:

    type someT struct {
        toUpper func(string) string
    }
    
    func New(toUpper func(string) string) *someT {
        if toUpper == nil {
            toUpper = strings.ToUpper
        }
        return &someT{
            toUpper: toUpper,
        }
    }
    
    func (s *someT) FuncToTest(t string) string {
        return s.toUpper(t)
    }
    

    这是一个关于如何注入特定函数的基本示例。在您的foo_test.go 文件中,您只需调用New,传递一个不同的函数。

    在更复杂的场景中,使用接口是最简单的方法。只需在测试文件中实现接口,并将替代实现传递给New函数:

    type StringProcessor interface {
        ToUpper(string) string
        Join([]string, string) string
        // all of the functions you need
    }
    
    func New(sp StringProcessor) return *someT {
        return &someT{
            processor: sp,
        }
    }
    

    从那时起,只需创建该接口的模拟实现,您就可以测试所有内容,而无需费力地反射。这使您的测试更易于维护,并且由于反射很复杂,因此您的测试出错的可能性大大降低。

    如果您的测试有问题,它可能会导致您的实际测试通过,即使您尝试测试的代码不起作用。如果测试代码比你开始覆盖的代码更复杂,我总是怀疑......

    【讨论】:

    • 为了记录,我只是在运行时为我的测试代码修改函数。有时很难覆盖返回错误的函数。
    • @hlin117:在这种情况下,我真诚地相信我们在这里处理的是 X-Y 问题。有很多方法可以在测试期间注入要使用的模拟函数。我会更新我的答案,添加一个简单的例子
    【解决方案2】:

    在幕后,Go 函数可能正如您所描述的那样 - 内存中一组指令的地址,并且在函数执行时根据系统的链接约定填充参数/返回值。

    然而,Go 的函数 抽象 是有目的的(这是语言设计决定)受到更多限制。您不能只替换函数,甚至覆盖其他导入包中的方法,就像在普通的面向对象语言中所做的那样。在正常情况下,您当然不能动态替换函数(我想您可以使用 unsafe 包写入任意内存位置,但这是对语言规则的故意规避,此时所有的赌注都没有了)。

    您是否正在尝试为单元测试执行某种依赖注入?如果是这样,在 Go 中执行此操作的惯用方法是定义传递给函数/方法的接口,并在测试中替换为测试版本。在您的情况下,接口可能会在正常实现中包装对 strings.ToUpper 的调用,但测试实现可能会调用其他东西。

    例如:

    type Upper interface {
       ToUpper(string) string
    }
    
    type defaultUpper struct {}
    
    func (d *defaultUpper) ToUpper(s string) string {
        return strings.ToUpper(s)
    }
    
    ...
    
    // normal implementation: pass in &defaultUpper{}
    // test implementation: pass in a test version that
    //      does something else
    func SomethingUseful(s string, d Upper) string {
        return d.ToUpper(s)
    }
    

    最后,您还可以传递函数值。例如:

    var fn func(string) string
    fn = strings.ToUpper
    
    ...
    
    fn("hello")
    

    ...当然,这不会让您替换系统的 strings.ToUpper 实现。

    无论哪种方式,您都只能通过接口或函数值来近似您想要在 Go 中执行的操作。它不像 Python,所有东西都是动态的和可替换的。

    【讨论】:

    • 是的,我正在尝试替换函数以进行单元测试。我知道惯用的解决方案是使用接口来模拟行为,但是注入接口会使代码更加混乱......
    • 不幸的是,您正在尝试做的是与语言的设计作斗争,而治疗方法可能比疾病更致命:(。此外,许多人(包括我自己)可能会争辩说,定义显式接口使你的代码更清晰,更清楚地传达你的设计意图。这是一种语言设计选择,Go 非常固执地以自己的方式做事。有些人讨厌它,但拥有一个代码库肯定是有价值的一致的风格和结构(在这种情况下在语言级别强制执行)。
    猜你喜欢
    • 1970-01-01
    • 2023-04-05
    • 1970-01-01
    • 2022-07-04
    • 2010-11-09
    • 2012-12-19
    • 1970-01-01
    • 2021-01-08
    • 2018-07-19
    相关资源
    最近更新 更多