『GCTT 出品』Go 中的匿名函数和反射

100 阅读25分钟
原文链接: mp.weixin.qq.com

我最近在浏览 Hacker News 时看到一篇吸引我眼球的文章《Python中的Lambdas和函数》,这篇文章 —— 我推荐你自己阅读一下 —— 详细讲解了如何运用 Python 的 lambda 函数,并举了一个例子展示如何使用 Lambda 函数实现干净,DRY 风格的代码。

读这篇文章,我大脑中喜欢设计模式的部分对文章里精巧的设计模式兴奋不已,然而同时,我大脑中讨厌动态语言的部分说,“呃~”。一点简短的题外话来表达一下我对动态语言的厌恶(如果你没有同感,请略过):

我曾经是一个动态语言的狂热粉丝(对某些任务我仍然喜欢动态语言并且几乎每天都会使用到它)。Python 是我大学一直选择的语言,我用它做科学计算并且做小的,概念验证的项目(我的个人网站曾经使用Flask)。但是当我在现实世界(Qadium)中开始为我的第一个大型 Python 项目做贡献时,一切都变了。这些项目包含了收集,处理并且增强各种定义好的数据类型的系统职责。

最开始我们选择 Python 基于两个原因:1)早期的员工都习惯使用 2)它是一门快速开发语言。

当我开始项目时,我们刚启动了我们最早的 B2B 产品,并且有几个很早就使用 Python 的开发者参与进来。留下来的代码有几个问题:1)代码很难读 2)代码很难调试 3)代码在不破坏些东西的前提下几乎无法改变/重构。而且,代码只经过了非常少的测试。这些就是快速搭建原型系统来验证我们第一个产品价值的代价了。上述提到的问题太严重了,以至于后来大部分的开发时间都用来定位解决问题,很少有宝贵的时间来开发新功能或者修改系统来满足我们不断增长的收集和处理数据的欲望和需要。

为了解决这些问题,我和另外一些工程师开始缓慢的,用一个静态类型语言重新架构和重写系统(对我当时来说,整体的体验就像是一边开着一辆着火的车,一边还要再建造另一辆新车)。对处理系统,我们选择了 Java 语言,对数据收集系统,我们选择了 Go 语言。两年后,我可以诚实的说,使用静态语言,比如 Go( Go 依然保留了很多动态语言的感觉,比如 Python )。

现在,我想肯定有不少读者在嘟囔着“像 Python 这样的动态语言是好的,只要把代码组织好并且测试好”这类的话了吧,我并不想较真这个观点,不过我要说的是,静态语言在解决我们系统的问题中帮了大忙,并且更适合我们的系统。当支持和修复好这边的麻烦事后,我们自己的基于 Python 的生成系统也做好了,我可以说我短期内都不想用动态语言来做任何的大项目了。

那么言归正传,当初我看到这篇文章时, 我看到里面有一些很棒的设计模式,我想试试看能否将它轻松的复制到 Go 中。如果你还没有读完上述提及的文章,我将它用 lambda /匿名函数解决的问题引述如下:

假设你的一个客户要求你写一个程序来模拟“逆波兰表达式计算器”,他们会将这个程序安装到他们全体员工的电脑上。你接受了这个任务,并且获得了这个程序的需求说明:

程序能做所有的基础运算(加减乘除),能求平方根和平方运算。很明显,你应该能清空计算器的所有堆栈或者只删除最后一个入栈的数值。

如果你对逆波兰表达式(RPN)不是很熟悉,可以在维基上或者找找它最开始的论文。

现在开始解决问题,之前的文章作者提供一个可用但是极度冗余的代码。把它移植到 Go 中,就是这样的

  1package main  2  3import (  4    "fmt"  5    "math"  6)  7  8func main() {  9    engine := NewRPMEngine() 10    engine.Push(2) 11    engine.Push(3) 12    engine.Compute("+") 13    engine.Compute("^2") 14    engine.Compute("SQRT") 15    fmt.Println("Result", engine.Pop()) 16} 17 18// RPMEngine 是一个 RPN 计算引擎 19type RPMEngine struct { 20    stack stack 21} 22 23// NewRPMEngine 返回一个 RPMEngine 24func NewRPMEngine() *RPMEngine { 25    return &RPMEngine{ 26        stack: make(stack, 0), 27    } 28} 29 30// 把一个值压入内部堆栈 31func (e *RPMEngine) Push(v int) { 32    e.stack = e.stack.Push(v) 33} 34 35// 把一个值从内部堆栈中取出 36func (e *RPMEngine) Pop() int { 37    var v int 38    e.stack, v = e.stack.Pop() 39    return v 40} 41 42// 计算一个运算 43// 如果这个运算返回一个值,把这个值压栈 44func (e *RPMEngine) Compute(operation string) error { 45    switch operation { 46    case "+": 47        e.addTwoNumbers() 48    case "-": 49        e.subtractTwoNumbers() 50    case "*": 51        e.multiplyTwoNumbers() 52    case "/": 53        e.divideTwoNumbers() 54    case "^2": 55        e.pow2ANumber() 56    case "SQRT": 57        e.sqrtANumber() 58    case "C": 59        e.Pop() 60    case "AC": 61        e.stack = make(stack, 0) 62    default: 63        return fmt.Errorf("Operation %s not supported", operation) 64    } 65    return nil 66} 67 68func (e *RPMEngine) addTwoNumbers() { 69    op2 := e.Pop() 70    op1 := e.Pop() 71    e.Push(op1 + op2) 72} 73 74func (e *RPMEngine) subtractTwoNumbers() { 75    op2 := e.Pop() 76    op1 := e.Pop() 77    e.Push(op1 - op2) 78} 79 80func (e *RPMEngine) multiplyTwoNumbers() { 81    op2 := e.Pop() 82    op1 := e.Pop() 83    e.Push(op1 * op2) 84} 85 86func (e *RPMEngine) divideTwoNumbers() { 87    op2 := e.Pop() 88    op1 := e.Pop() 89    e.Push(op1 * op2) 90} 91 92func (e *RPMEngine) pow2ANumber() { 93    op1 := e.Pop() 94    e.Push(op1 * op1) 95} 96 97func (e *RPMEngine) sqrtANumber() { 98    op1 := e.Pop() 99    e.Push(int(math.Sqrt(float64(op1))))100}

rpn_calc_solution1.go 由 GitHub 托管,查看源文件

注:Go 并没有一个自带的堆栈,所以,我自己创建了一个。

 1package main 2 3type stack []int 4 5func (s stack) Push(v int) stack { 6    return append(s, v) 7} 8 9func (s stack) Pop() (stack, int) {10    l := len(s)11    return s[:l-1], s[l-1]12}

simple_stack.go 由 GitHub 托管,查看源文件

(另外,这个堆栈不是线程安全的,并且对空堆栈进行 Pop 操作会引发 panic,除此之外,这个堆栈工作的很好)

以上的方案是可以工作的,但是有大堆的代码重复 —— 特别是获取提供给运算符的参数/操作的代码。

Python-lambda 文章对这个方案做了一个改进,将运算函数写为 lambda 表达式并且放入一个字典中,这样它们可以通过名称来引用,在运行期查找一个运算所需要操作的数值,并用普通的代码将这些操作数提供给运算函数。最终的python代码如下:

 1""" 2Engine class of the RPN Calculator 3""" 4 5import math 6from inspect import signature 7 8class rpn_engine: 9    def __init__(self):10        """ Constructor """11        self.stack = []12        self.catalog = self.get_functions_catalog()1314    def get_functions_catalog(self):15        """ Returns the catalog of all the functions supported by the calculator """16        return {"+": lambda x, y: x + y,17                "-": lambda x, y: x - y,18                "*": lambda x, y: x * y,19                "/": lambda x, y: x / y,20                "^2": lambda x: x * x,21                "SQRT": lambda x: math.sqrt(x),22                "C": lambda: self.stack.pop(),23                "AC": lambda: self.stack.clear()}2425    def push(self, number):26        """ push a value to the internal stack """27        self.stack.append(number)2829    def pop(self):30        """ pop a value from the stack """31        try:32            return self.stack.pop()33        except IndexError:34            pass # do not notify any error if the stack is empty...3536    def compute(self, operation):37        """ compute an operation """3839        function_requested = self.catalog[operation]40        number_of_operands = 041        function_signature = signature(function_requested)42        number_of_operands = len(function_signature.parameters)4344        if number_of_operands == 2:45            self.compute_operation_with_two_operands(self.catalog[operation])4647        if number_of_operands == 1:48            self.compute_operation_with_one_operand(self.catalog[operation])4950        if number_of_operands == 0:51            self.compute_operation_with_no_operands(self.catalog[operation])5253    def compute_operation_with_two_operands(self, operation):54        """ exec operations with two operands """55        try:56            if len(self.stack) < 2:57                raise BaseException("Not enough operands on the stack")5859            op2 = self.stack.pop()60            op1 = self.stack.pop()61            result = operation(op1, op2)62            self.push(result)63        except BaseException as error:64            print(error)6566    def compute_operation_with_one_operand(self, operation):67        """ exec operations with one operand """68        try:69            op1 = self.stack.pop()70            result = operation(op1)71            self.push(result)72        except BaseException as error:73            print(error)7475    def compute_operation_with_no_operands(self, operation):76        """ exec operations with no operands """77        try:78            operation()79        except BaseException as error:80            print(error)

engine_peter_rel5.py 由GitHub托管 查看源文件

这个方案比原来的方案只增加了一点点复杂度,但是现在增加一个新的运算符简直就像增加一条线一样简单!我看到这个的第一个想法就是:我怎么在 Go 中实现?

我知道在 Go 中有函数字面量,它是一个很简单的东西,就像在 Python 的方案中,创建一个运算符的名字与运算符操作的 map。它可以这么被实现:

 1package main 2 3import "math" 4 5func main() { 6    catalog := map[string]interface{}{ 7        "+":    func(x, y int) int { return x + y }, 8        "-":    func(x, y int) int { return x - y }, 9        "*":    func(x, y int) int { return x * y },10        "/":    func(x, y int) int { return x / y },11        "^2":   func(x int) int { return x * x },12        "SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },13        "C":    func() { /* TODO: need engine object */ },14        "AC":   func() { /* TODO: need engine object */ },15    }16}17view rawrpn_operations_map.go hosted with ❤ by GitHub

rpn_operations_map.go 由gitHub托管 查看源文件

注意:在 Go 语言中,为了将我们所有的匿名函数保存在同一个 map 中,我们需要使用空接口类型,interfa{}。在 Go 中所有类型都实现了空接口(它是一个没有任何方法的接口;所有类型都至少有 0 个函数)。在底层,Go 用两个指针来表示一个接口:一个指向值,另一个指向类型。

识别接口实际保存的类型的一个方法是用 .(type) 来做断言,比如:

 1package main 2 3import ( 4    "fmt" 5    "math" 6) 7 8func main() { 9    catalog := map[string]interface{} {10        "+":    func(x, y int) int { return x + y },11        "-":    func(x, y int) int { return x - y },12        "*":    func(x, y int) int { return x * y },13        "/":    func(x, y int) int { return x / y },14        "^2":   func(x int) int { return x * x },15        "SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },16        "C":    func() { /* TODO: need engine object */ },17        "AC":   func() { /* TODO: need engine object */ },18    }1920    for k, v := range catalog {21        switch v.(type) {22        case func(int, int) int:23            fmt.Printf("%s takes two operands\n", k)24        case func(int) int:25            fmt.Printf("%s takes one operands\n", k)26        case func():27            fmt.Printf("%s takes zero operands\n", k)28        }29    }30}31

rpn_operations_map2.go 由GitHub托管 查看源文件

这段代码会产生如下输出(请原谅语法上的瑕疵):

1SQRT takes one operands2AC takes zero operands3+ takes two operands4/ takes two operands5^2 takes one operands6- takes two operands7* takes two operands8C takes zero operands

这就揭示了一种方法,可以获得一个运算符需要多少个操作数,以及如何复制 Python 的解决方案。但是我们如何能做到更好?我们能否为提取运算符所需参数抽象出一个更通用的逻辑?我们能否在不用 if 或者 switch 语句的情况下,查找一个函数所需要的操作数的个数并且调用它?实际上通过 Go 中的 relect 包提供的反射功能,我们是可以做到的。

对于 Go 中的反射,一个简要的说明如下:

在 Go 中,通常来讲,如果你需要一个变量,类型或者函数,你可以定义它然后使用它。然而,如果你发现你是在运行时需要它们,或者你在设计一个系统需要使用多种不同类型(比如,实现运算符的函数 —— 它们接受不同数量的变量,因此是不同的类型),那么你可以就需要使用反射。反射给你在运行时检查,创建和修改不同类型的能力。如果需要更详尽的 Go 的反射说明以及一些使用 reflect 包的基础知识,请参阅反射的规则这篇博客。

下列代码演示了另一种解决方法,通过反射来实现查找我们匿名函数需要的操作数的个数:

 1package main 2 3import ( 4    "fmt" 5    "math" 6    "reflect" 7) 8 9func main() {10    catalog := map[string]interface{}{11        "+":    func(x, y int) int { return x + y },12        "-":    func(x, y int) int { return x - y },13        "*":    func(x, y int) int { return x * y },14        "/":    func(x, y int) int { return x / y },15        "^2":   func(x int) int { return x * x },16        "SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },17        "C":    func() { /* TODO: need engine object */ },18        "AC":   func() { /* TODO: need engine object */ },19    }2021    for k, v := range catalog {22        method := reflect.ValueOf(v)23        numOperands := method.Type().NumIn()24        fmt.Printf("%s has %d operands\n", k, numOperands)25    }26}

rpn_operations_map3.go 由GitHub托管 查看源文件

类似与用 .(type) 来切换的方法,代码输出如下:

1^2 has 1 operands2SQRT has 1 operands3AC has 0 operands4* has 2 operands5/ has 2 operands6C has 0 operands7+ has 2 operands8- has 2 operands

现在我不再需要根据函数的签名来硬编码参数的数量了!

注意:如果值的种类(种类(Kind) 不要与类型弄混了))不是 Func,调用 toNumIn 会触发 panic,所以小心使用,因为 panic 只有在运行时才会发生。

通过检查 Go 的 reflect 包,我们知道,如果一个值的种类(Kind)是 Func 的话,我们是可以通过调用 Call 方法,并且传给它一个 值对象的切片来调用这个函数。比如,我们可以这么做:

 1package main 2 3import ( 4    "fmt" 5    "math" 6    "reflect" 7) 8 9func main() {10    catalog := map[string]interface{}{11        "+":    func(x, y int) int { return x + y },12        "-":    func(x, y int) int { return x - y },13        "*":    func(x, y int) int { return x * y },14        "/":    func(x, y int) int { return x / y },15        "^2":   func(x int) int { return x * x },16        "SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },17        "C":    func() { /* TODO: need engine object */ },18        "AC":   func() { /* TODO: need engine object */ },19    }2021    method := reflect.ValueOf(catalog["+"])22    operands := []reflect.Value{23        reflect.ValueOf(3),24        reflect.ValueOf(2),25    }2627    results := method.Call(operands)28    fmt.Println("The result is ", int(results[0].Int()))29}

rpn_operations_map4.go 由GitHub托管 查看源文件

就像我们期待的那样,这段代码会输出:

1The result is  5

酷!

现在我们可以写出我们的终极解决方案了:

 1package main 2 3import ( 4    "fmt" 5    "math" 6    "reflect" 7) 8 9func main() {10    engine := NewRPMEngine()11    engine.Push(2)12    engine.Push(3)13    engine.Compute("+")14    engine.Compute("^2")15    engine.Compute("SQRT")16    fmt.Println("Result", engine.Pop())17}1819// RPMEngine 是一个 RPN 计算引擎20type RPMEngine struct {21    stack   stack22    catalog map[string]interface{}23}2425// NewRPMEngine 返回 一个 带有 缺省功能目录的 RPMEngine26func NewRPMEngine() *RPMEngine {27    engine := &RPMEngine{28        stack: make(stack, 0),29    }30    engine.catalog = map[string]interface{}{31        "+":    func(x, y int) int { return x + y },32        "-":    func(x, y int) int { return x - y },33        "*":    func(x, y int) int { return x * y },34        "/":    func(x, y int) int { return x / y },35        "^2":   func(x int) int { return x * x },36        "SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },37        "C":    func() { _ = engine.Pop() },38        "AC":   func() { engine.stack = make(stack, 0) },39    }40    return engine41}4243// 将一个值压入内部堆栈44func (e *RPMEngine) Push(v int) {45    e.stack = e.stack.Push(v)46}4748// 从内部堆栈取出一个值49func (e *RPMEngine) Pop() int {50    var v int51    e.stack, v = e.stack.Pop()52    return v53}5455// 计算一个运算56// 如果这个运算返回一个值,把这个值压栈57func (e *RPMEngine) Compute(operation string) error {58    opFunc, ok := e.catalog[operation]59    if !ok {60        return fmt.Errorf("Operation %s not supported", operation)61    }6263    method := reflect.ValueOf(opFunc)64    numOperands := method.Type().NumIn()65    if len(e.stack) < numOperands {66        return fmt.Errorf("Too few operands for requested operation %s", operation)67    }6869    operands := make([]reflect.Value, numOperands)70    for i := 0; i < numOperands; i++ {71        operands[numOperands-i-1] = reflect.ValueOf(e.Pop())72    }7374    results := method.Call(operands)75    // If the operation returned a result, put it on the stack76    if len(results) == 1 {77        result := results[0].Int()78        e.Push(int(result))79    }8081    return nil82}

rpn_calc_solution2.go 由 GitHub托管 查看源文件

我们确定操作数的个数(第 64 行),从堆栈中获得操作数(第 69-72 行),然后调用需要的运算函数,而且对不同参数个数的运算函数的调用都是一样的(第 74 行)。而且与 Python 的解决方案一样,增加新的运算函数,只需要往 map 中增加一个匿名函数的条目就可以了(第 30 行)。

总结一下,我们已经知道如果使用匿名函数和反射将一个 Python 中有趣的设计模式复制到 Go 语言中来。对反射的过度使用我会保持警惕,反射增加了代码的复杂度,而且我经常看到反射用于绕过一些坏的设计。另外,它会将一些本该在编译期发生的错误变成运行期错误,而且它还会显著拖慢程序(即将推出:两种方案的基准检查) —— 通过检查源码,看起来 reflect.Value.Call 执行了很多的准备工作,并且对每一次调用 都为它的 []reflect.Value 返回结果参数分配了新的切片。也就是说,如果性能不需要关注 —— 在 Python 中通常都不怎么关注性能;),有足够的测试,而且我们的目标是优化代码的长度,并且想让它易于加入新的运算,那么反射是一个值得推荐的方法。


via:https://medium.com/@jhh3/anonymous-functions-and-reflection-in-go-71274dd9e83a

作者:John Holliman  译者:MoodWu  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出