我一直在使用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
函数。
答案 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)
我最近才开始深入研究golang编译器,更具体地说:go汇编器及其映射。因为我不是专家,所以我不打算在这里解释所有细节(因为我的知识很可能仍然缺乏)。我将在底部提供一些可能值得查看的链接以获取更多详细信息。
你要做的事对我来说非常有意义。如果在运行时,您正在尝试修改函数,那么您可能在之前做错了。这就是为了防止你想要搞乱任何功能。您尝试使用strings
包中的函数执行某些操作的事实使得这更加令人担忧。 reflect
包允许您编写非常通用的函数(例如,带有请求处理程序的服务,但是您希望将任意参数传递给那些处理程序,要求您拥有一个处理程序,处理原始请求,然后调用相应的处理程序你不可能知道那个处理程序是什么样的,所以你使用反射来计算出所需的参数......)。
现在,如何实现功能?
go编译器是一个棘手的代码库,可以解决这些问题,但幸运的是语言设计及其实现已经公开讨论过。从我收集的内容来看,golang函数本质上的编译方式与C中的函数几乎完全相同,但是,调用函数有点不同。 Go函数是第一类对象,这就是为什么你可以将它们作为参数传递,声明一个函数类型,以及为什么reflect
包必须允许你对函数参数使用反射。
基本上,功能不是直接解决的。函数通过函数“pointer”传递和调用。函数实际上类似于map
或slice
。它们包含指向实际代码和调用数据的指针。简单来说,将函数视为一种类型(伪代码):
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},
},
}
然后,通过分配与args
和rVal
约束匹配的任何函数,您可以确定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,
}
}
从那时起,只需创建该接口的模拟实现,您就可以测试所有内容,而无需使用反射进行测试。这使得您的测试更容易维护,并且因为反射很复杂,所以它使测试错误的可能性大大降低。
如果您的测试有问题,即使您尝试测试的代码无效,也可能导致您的实际测试通过。如果测试代码比您开始使用的代码更复杂,我总是怀疑......