和其他编程语言中的函数相比,Go 语言的函数具有如下特点:
- 以“func”关键字开头;
- 支持多返回值;
- 支持具名返回值;
- 支持递归调用;
- 支持同类型的可变参数;
- 支持 defer,实现函数优雅返回
更为关键的是函数在 Go 语言中属于“一等公民(first-class citizen)”。众所周知,并不是在所有编程语言中函数都是“一等公民”,本节中我就和大家一起来看看成为”一等公民“的函数都有哪些特质可以帮助我们写出优雅简洁的代码。
1. 什么是“一等公民”
关于什么是编程语言的“一等公民”,业界并没有教科书给出精准的定义。这里引用一下 wiki 发明人、C2 站点作者沃德·坎宁安(Ward Cunningham)对“一等公民”的诠释:
如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为函数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。
基于上面关于“一等公民”的诠释,我们来看看 Go 语言的函数是如何满足上述条件而成为“一等公民”的。
- 正常创建
// $GOROOT/src/fmt/print.gofunc newPrinter() *pp {p := ppFree.Get().(*pp)p.panicking = falsep.erroring = falsep.wrapErrs = falsep.fmt.init(&p.buf)return p}
- 在函数内创建
// $GOROOT/src/runtime/print.gofunc hexdumpWords(p, end uintptr, mark func(uintptr) byte) {p1 := func(x uintptr) {var buf [2 * sys.PtrSize]bytefor i := len(buf) - 1; i >= 0; i-- {if x&0xF < 10 {buf[i] = byte(x&0xF) + '0'} else {buf[i] = byte(x&0xF) - 10 + 'a'}x >>= 4}gwrite(buf[:])}... ...}
- 作为类型
// $GOROOT/src/net/http/server.gotype HandlerFunc func(ResponseWriter, *Request)// $GOROOT/src/sort/genzfunc.gotype visitFunc func(ast.Node) ast.Visitor// codewalk: https://tip.golang.org/doc/codewalk/functions/type action func(current score) (result score, turnIsOver bool)
- 存储到变量中
// $GOROOT/src/runtime/vdso_linux.gofunc vdsoParseSymbols(info *vdsoInfo, version int32) {if !info.valid {return}apply := func(symIndex uint32, k vdsoSymbolKey) bool {sym := &info.symtab[symIndex]typ := _ELF_ST_TYPE(sym.st_info)bind := _ELF_ST_BIND(sym.st_info)... ...*k.ptr = info.loadOffset + uintptr(sym.st_value)return true}... ...}
- 作为参数传入函数
$GOROOT/src/time/sleep.gofunc AfterFunc(d Duration, f func()) *Timer {t := &Timer{r: runtimeTimer{when: when(d),f: goFunc,arg: f,},}startTimer(&t.r)return t}
- 作为返回值从函数返回
// $GOROOT/src/strings/strings.gofunc makeCutsetFunc(cutset string) func(rune) bool {if len(cutset) == 1 && cutset[0] < utf8.RuneSelf {return func(r rune) bool {return r == rune(cutset[0])}}if as, isASCII := makeASCIISet(cutset); isASCII {return func(r rune) bool {return r < utf8.RuneSelf && as.contains(byte(r))}}return func(r rune) bool { return IndexRune(cutset, r) >= 0 }}
除了上面那些例子,函数还可以被放入数组/切片/map 等结构中、可以像其他类型变量一样被赋值给 interface{}、甚至我们可以建立元素为函数的 channel,如下面例子:
// function_as_first_class_citizen_1.gopackage mainimport "fmt"type binaryCalcFunc func(int, int) intfunc main() {var i interface{} = binaryCalcFunc(func(x, y int) int { return x + y })c := make(chan func(int, int) int, 10)fns := []binaryCalcFunc{func(x, y int) int { return x + y },func(x, y int) int { return x - y },func(x, y int) int { return x * y },func(x, y int) int { return x / y },func(x, y int) int { return x % y },}c <- func(x, y int) int {return x * y}fmt.Println(fns[0](5, 6))f := <-cfmt.Println(f(7, 10))v, ok := i.(binaryCalcFunc)if !ok {fmt.Println("type assertion error")return}fmt.Println(v(17, 7))}
2. 函数作为“一等公民”的特殊运用
1). 像整型变量那样对函数进行显式转型
Go 是类型安全的语言,Go 语言不允许隐式类型转换,因此下面的代码是无法通过编译的:
var a int = 5var b int32 = 6fmt.Println(a + b) // 违法操作: a + b (不匹配的类型int和int32)
我们必须通过对上面代码进行显式的转型才能通过编译器的检查:
var a int = 5var b int32 = 6fmt.Println(a + int(b)) // ok。输出11
函数是“一等公民”,对整型变量进行的操作也同样可以用在函数上面,即函数也可以被显式转型,并且这样的转型在特定的领域具有奇妙的作用。一个最为典型的示例就是 http.HandlerFunc 这个类型,我们来看一下例子:
// function_as_first_class_citizen_2.gopackage mainimport ("fmt""net/http")func greeting(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Welcome, Gopher!\n")}func main() {http.ListenAndServe(":8080", http.HandlerFunc(greeting))}
ListenAndServe 的源码:
// $GOROOT/src/net/http/server.gofunc ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()}
ListenAndServe 会将来自客户端的 http 请求交给其第二个参数 handler 处理,而这里 handler 参数的类型 http.Handler 接口:
// $GOROOT/src/net/http/server.gotype Handler interface {ServeHTTP(ResponseWriter, *Request)}
该接口仅有一个方法:ServeHTTP,其原型为:func(http.ResponseWriter, http.Request)。这与我们自己定义的 http 请求处理函数 greeting 的原型是一致的。*但是我们没法直接将 greeting 作为参数值传入,否则会报下面错误:
func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)
http.HandlerFunc 是什么?
// $GOROOT/src/net/http/server.gotype HandlerFunc func(ResponseWriter, *Request)// ServeHTTP calls f(w, r).func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {f(w, r)}
2) 函数式编程
Go 语言演进到如今,对多种编程范式或多或少都有支持。比如:对函数式编程的支持就得意于函数是“一等公民”的特质。虽然 Go 不推崇函数式编程,但有些时候应用一些函数式编程风格可以写出更加优雅、更简洁、更易维护的代码。
柯里化函数
我们先来看一种函数式编程的典型应用:柯里化函数(currying)。在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(原函数的第一个参数)的函数,并且返回接受余下的参数和返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名。
定义总是拗口难懂,我们来用 Go 编写一个直观的柯里化函数的例子:
// function_as_first_class_citizen_4.gopackage mainimport "fmt"func times(x, y int) int {return x * y}func partialTimes(x int) func(int) int {return func(y int) int {return times(x, y)}}func main() {timesTwo := partialTimes(2)timesThree := partialTimes(3)timesFour := partialTimes(4)fmt.Println(timesTwo(5))fmt.Println(timesThree(5))fmt.Println(timesFour(5))}
运行这个例子:
$ go run function_as_first_class_citizen_4.go101520
这个例子利用了函数的几点性质:
- 在函数中定义,通过返回值返回
- 闭包
闭包是前面没有提到的 Go 函数支持的一个特性。 闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。本质上,闭包是将函数内部和函数外部连接起来的桥梁。
函子(Functor)
什么是函子呢?具体来说,成为函子需要两个条件:
- 函子本身是一个容器类型,以 Go 语言为例,这个容器可以是切片、map 甚至是 channel;
- 光是容器还不够,该容器类型还需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新的函子,原函子容器内部的元素值不受到影响
// function_as_first_class_citizen_5.gopackage mainimport ("fmt")type IntSliceFunctor interface {Fmap(fn func(int) int) IntSliceFunctor // 注意!}type intSliceFunctorImpl struct {ints []int}func (isf intSliceFunctorImpl) Fmap(fn func(int) int) IntSliceFunctor {newInts := make([]int, len(isf.ints))for i, elt := range isf.ints { // 将原有元素应用 fn, 新元素集合成为新的 IntSliceFunctorretInt := fn(elt)newInts[i] = retInt}return intSliceFunctorImpl{ints: newInts}}func NewIntSliceFunctor(slice []int) IntSliceFunctor {return intSliceFunctorImpl{ints: slice}}func main() {// 原切片intSlice := []int{1, 2, 3, 4}fmt.Printf("init a functor from int slice: %#v\n", intSlice)f := NewIntSliceFunctor(intSlice)fmt.Printf("original functor: %+v\n", f)mapperFunc1 := func(i int) int {return i + 10}mapped1 := f.Fmap(mapperFunc1)fmt.Printf("mapped functor1: %+v\n", mapped1)mapperFunc2 := func(i int) int {return i * 3}mapped2 := mapped1.Fmap(mapperFunc2)fmt.Printf("mapped functor2: %+v\n", mapped2)fmt.Printf("original functor: %+v\n", f) // 原functor没有改变fmt.Printf("composite functor: %+v\n", f.Fmap(mapperFunc1).Fmap(mapperFunc2))}
运行这段代码:
$ go run function_as_first_class_citizen_5.goinit a functor from int slice: []int{1, 2, 3, 4}original functor: {ints:[1 2 3 4]}mapped functor1: {ints:[11 12 13 14]}mapped functor2: {ints:[33 36 39 42]}original functor: {ints:[1 2 3 4]}composite functor: {ints:[33 36 39 42]}
functor 非常适合对容器集合元素做批量同构处理,而且代码也要比每次都对容器中的元素作循环处理要优雅简洁许多。但要想在 Go 中发挥 functor 的最大效能,还需要 Go 对泛型提供支持,否则我们就需要为每一种容器类型都实现一套对应的 Functor 机制。比如上面的示例仅支持元素类型为 int 的切片,如果元素类型换为 string 或元素类型依然为 int,但容器类型换为 map,我们还需要分别为之编写新的配套代码。
延续传递式(Continuation-passing Style)
函数式编程离不开递归,以求阶乘函数为例,我们可以轻易用递归方法写出一个实现:
// function_as_first_class_citizen_6.gofunc factorial(n int) int {if n == 1 {return 1} else {return n * factorial(n-1)}}func main() {fmt.Printf("%d\n", factorial(5))}
在 CPS 风格中,函数是不允许有返回值的。一个函数 A 应该将其想返回的值显式传给一个 continuation 函数(一般接受一个参数),而这个 continuation 函数自身是函数 A 的一个参数。概念太过抽象,我们用一个简单的例子来说明一下:
下面得 Max 函数的功能是返回两个参数值中较大的那个值:
// function_as_first_class_citizen_7.gopackage mainimport "fmt"func Max(n int, m int) int {if n > m {return n} else {return m}}func main() {fmt.Printf("%d\n", Max(5, 6))}
我们把 Max 函数看作是上面定义中的 A 函数在未 CPS 化之前的状态。接下来,我们来根据 CPS 的定义将其转换为 CPS 风格:
- 首先我们去掉 Max 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 就是定义中的 continuation 函数)
func Max(n int, m int, f func(int))
- 将返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用
func Max(n int, m int, f func(int)) {if n > m {f(n)} else {f(m)}}
完整的转换后的代码如下:
// function_as_first_class_citizen_8.gopackage mainimport "fmt"func Max(n int, m int, f func(y int)) {if n > m {f(n)} else {f(m)}}func main() {Max(5, 6, func(y int) { fmt.Printf("%d\n", y) })}
接下来,我们使用同样的方法将上面的阶乘实现转换为 CPS 风格。
- 首先我们去掉 factorial 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 也就是 CPS 定义中的 continuation 函数)
func factorial(n int, f func(y int))
- 接下来,将 factorial 实现中的返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用
func factorial(n int, f func(int)) {if n == 1 {f(n)} else {factorial(n-1, func(y int) { f(n * y) })}}
转换为 CPS 风格的阶乘函数的完整代码如下:
/ function_as_first_class_citizen_9.gopackage mainimport "fmt"func factorial(n int, f func(int)) {if n == 1 {f(1) //基本情况} else {factorial(n-1, func(y int) { f(n * y) })}}func main() {factorial(5, func(y int) { fmt.Printf("%d\n", y) })}
