功能如何实现?

时间:2017-12-10 01:30:16

标签: function pointers go

我一直在使用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函数。

2 个答案:

答案 0 :(得分:1)

在封面下,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,一切都是动态的和可替换的。

答案 1 :(得分:1)

免责声明:

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

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

  3. 现在,如何实现功能?

    go编译器是一个棘手的代码库,可以解决这些问题,但幸运的是语言设计及其实现已经公开讨论过。从我收集的内容来看,golang函数本质上的编译方式与C中的函数几乎完全相同,但是,调用函数有点不同。 Go函数是第一类对象,这就是为什么你可以将它们作为参数传递,声明一个函数类型,以及为什么reflect包必须允许你对函数参数使用反射。

    基本上,功能不是直接解决的。函数通过函数“pointer”传递和调用。函数实际上类似于mapslice。它们包含指向实际代码和调用数据的指针。简单来说,将函数视为一种类型(伪代码):

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

    这意味着reflect包可用于,例如,计算函数所需的参数和返回值的数量。它还告诉您返回值是什么。整个函数“type”将能够告诉你函数所在的位置,以及它期望和返回的参数,但这就是它。 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,
        }
    }
    

    从那时起,只需创建该接口的模拟实现,您就可以测试所有内容,而无需使用反射进行测试。这使得您的测试更容易维护,并且因为反射很复杂,所以它使测试错误的可能性大大降低。

    如果您的测试有问题,即使您尝试测试的代码无效,也可能导致您的实际测试通过。如果测试代码比您开始使用的代码更复杂,我总是怀疑......