和其他编程语言中的函数相比,Go 语言的函数具有如下特点:

  • 以“func”关键字开头;
  • 支持多返回值;
  • 支持具名返回值;
  • 支持递归调用;
  • 支持同类型的可变参数;
  • 支持 defer,实现函数优雅返回

更为关键的是函数在 Go 语言中属于“一等公民(first-class citizen)”。众所周知,并不是在所有编程语言中函数都是“一等公民”,本节中我就和大家一起来看看成为”一等公民“的函数都有哪些特质可以帮助我们写出优雅简洁的代码。

1. 什么是“一等公民”

关于什么是编程语言的“一等公民”,业界并没有教科书给出精准的定义。这里引用一下 wiki 发明人、C2 站点作者沃德·坎宁安(Ward Cunningham)对“一等公民”的诠释:

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为函数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。

基于上面关于“一等公民”的诠释,我们来看看 Go 语言的函数是如何满足上述条件而成为“一等公民”的。

  • 正常创建
  1. // $GOROOT/src/fmt/print.go
  2. func newPrinter() *pp {
  3. p := ppFree.Get().(*pp)
  4. p.panicking = false
  5. p.erroring = false
  6. p.wrapErrs = false
  7. p.fmt.init(&p.buf)
  8. return p
  9. }
  • 在函数内创建
  1. // $GOROOT/src/runtime/print.go
  2. func hexdumpWords(p, end uintptr, mark func(uintptr) byte) {
  3. p1 := func(x uintptr) {
  4. var buf [2 * sys.PtrSize]byte
  5. for i := len(buf) - 1; i >= 0; i-- {
  6. if x&0xF < 10 {
  7. buf[i] = byte(x&0xF) + '0'
  8. } else {
  9. buf[i] = byte(x&0xF) - 10 + 'a'
  10. }
  11. x >>= 4
  12. }
  13. gwrite(buf[:])
  14. }
  15. ... ...
  16. }
  • 作为类型
  1. // $GOROOT/src/net/http/server.go
  2. type HandlerFunc func(ResponseWriter, *Request)
  3. // $GOROOT/src/sort/genzfunc.go
  4. type visitFunc func(ast.Node) ast.Visitor
  5. // codewalk: https://tip.golang.org/doc/codewalk/functions/
  6. type action func(current score) (result score, turnIsOver bool)
  • 存储到变量中
  1. // $GOROOT/src/runtime/vdso_linux.go
  2. func vdsoParseSymbols(info *vdsoInfo, version int32) {
  3. if !info.valid {
  4. return
  5. }
  6. apply := func(symIndex uint32, k vdsoSymbolKey) bool {
  7. sym := &info.symtab[symIndex]
  8. typ := _ELF_ST_TYPE(sym.st_info)
  9. bind := _ELF_ST_BIND(sym.st_info)
  10. ... ...
  11. *k.ptr = info.loadOffset + uintptr(sym.st_value)
  12. return true
  13. }
  14. ... ...
  15. }
  • 作为参数传入函数
  1. $GOROOT/src/time/sleep.go
  2. func AfterFunc(d Duration, f func()) *Timer {
  3. t := &Timer{
  4. r: runtimeTimer{
  5. when: when(d),
  6. f: goFunc,
  7. arg: f,
  8. },
  9. }
  10. startTimer(&t.r)
  11. return t
  12. }
  • 作为返回值从函数返回
  1. // $GOROOT/src/strings/strings.go
  2. func makeCutsetFunc(cutset string) func(rune) bool {
  3. if len(cutset) == 1 && cutset[0] < utf8.RuneSelf {
  4. return func(r rune) bool {
  5. return r == rune(cutset[0])
  6. }
  7. }
  8. if as, isASCII := makeASCIISet(cutset); isASCII {
  9. return func(r rune) bool {
  10. return r < utf8.RuneSelf && as.contains(byte(r))
  11. }
  12. }
  13. return func(r rune) bool { return IndexRune(cutset, r) >= 0 }
  14. }

除了上面那些例子,函数还可以被放入数组/切片/map 等结构中、可以像其他类型变量一样被赋值给 interface{}、甚至我们可以建立元素为函数的 channel,如下面例子:

  1. // function_as_first_class_citizen_1.go
  2. package main
  3. import "fmt"
  4. type binaryCalcFunc func(int, int) int
  5. func main() {
  6. var i interface{} = binaryCalcFunc(func(x, y int) int { return x + y })
  7. c := make(chan func(int, int) int, 10)
  8. fns := []binaryCalcFunc{
  9. func(x, y int) int { return x + y },
  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. }
  15. c <- func(x, y int) int {
  16. return x * y
  17. }
  18. fmt.Println(fns[0](5, 6))
  19. f := <-c
  20. fmt.Println(f(7, 10))
  21. v, ok := i.(binaryCalcFunc)
  22. if !ok {
  23. fmt.Println("type assertion error")
  24. return
  25. }
  26. fmt.Println(v(17, 7))
  27. }

2. 函数作为“一等公民”的特殊运用

1). 像整型变量那样对函数进行显式转型

Go 是类型安全的语言,Go 语言不允许隐式类型转换,因此下面的代码是无法通过编译的:

  1. var a int = 5
  2. var b int32 = 6
  3. fmt.Println(a + b) // 违法操作: a + b (不匹配的类型int和int32)

我们必须通过对上面代码进行显式的转型才能通过编译器的检查:

  1. var a int = 5
  2. var b int32 = 6
  3. fmt.Println(a + int(b)) // ok。输出11

函数是“一等公民”,对整型变量进行的操作也同样可以用在函数上面,即函数也可以被显式转型,并且这样的转型在特定的领域具有奇妙的作用。一个最为典型的示例就是 http.HandlerFunc 这个类型,我们来看一下例子:

  1. // function_as_first_class_citizen_2.go
  2. package main
  3. import (
  4. "fmt"
  5. "net/http"
  6. )
  7. func greeting(w http.ResponseWriter, r *http.Request) {
  8. fmt.Fprintf(w, "Welcome, Gopher!\n")
  9. }
  10. func main() {
  11. http.ListenAndServe(":8080", http.HandlerFunc(greeting))
  12. }

ListenAndServe 的源码:

  1. // $GOROOT/src/net/http/server.go
  2. func ListenAndServe(addr string, handler Handler) error {
  3. server := &Server{Addr: addr, Handler: handler}
  4. return server.ListenAndServe()
  5. }

ListenAndServe 会将来自客户端的 http 请求交给其第二个参数 handler 处理,而这里 handler 参数的类型 http.Handler 接口:

  1. // $GOROOT/src/net/http/server.go
  2. type Handler interface {
  3. ServeHTTP(ResponseWriter, *Request)
  4. }

该接口仅有一个方法:ServeHTTP,其原型为:func(http.ResponseWriter, http.Request)。这与我们自己定义的 http 请求处理函数 greeting 的原型是一致的。*但是我们没法直接将 greeting 作为参数值传入,否则会报下面错误:

  1. func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)

http.HandlerFunc 是什么?

  1. // $GOROOT/src/net/http/server.go
  2. type HandlerFunc func(ResponseWriter, *Request)
  3. // ServeHTTP calls f(w, r).
  4. func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  5. f(w, r)
  6. }

2) 函数式编程

Go 语言演进到如今,对多种编程范式或多或少都有支持。比如:对函数式编程的支持就得意于函数是“一等公民”的特质。虽然 Go 不推崇函数式编程,但有些时候应用一些函数式编程风格可以写出更加优雅、更简洁、更易维护的代码。

柯里化函数

我们先来看一种函数式编程的典型应用:柯里化函数(currying)。在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(原函数的第一个参数)的函数,并且返回接受余下的参数和返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名。

定义总是拗口难懂,我们来用 Go 编写一个直观的柯里化函数的例子:

  1. // function_as_first_class_citizen_4.go
  2. package main
  3. import "fmt"
  4. func times(x, y int) int {
  5. return x * y
  6. }
  7. func partialTimes(x int) func(int) int {
  8. return func(y int) int {
  9. return times(x, y)
  10. }
  11. }
  12. func main() {
  13. timesTwo := partialTimes(2)
  14. timesThree := partialTimes(3)
  15. timesFour := partialTimes(4)
  16. fmt.Println(timesTwo(5))
  17. fmt.Println(timesThree(5))
  18. fmt.Println(timesFour(5))
  19. }

运行这个例子:

  1. $ go run function_as_first_class_citizen_4.go
  2. 10
  3. 15
  4. 20

这个例子利用了函数的几点性质:

  • 在函数中定义,通过返回值返回
  • 闭包

闭包是前面没有提到的 Go 函数支持的一个特性。 闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。本质上,闭包是将函数内部和函数外部连接起来的桥梁

函子(Functor)

什么是函子呢?具体来说,成为函子需要两个条件:

  • 函子本身是一个容器类型,以 Go 语言为例,这个容器可以是切片、map 甚至是 channel;
  • 光是容器还不够,该容器类型还需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新的函子,原函子容器内部的元素值不受到影响
  1. // function_as_first_class_citizen_5.go
  2. package main
  3. import (
  4. "fmt"
  5. )
  6. type IntSliceFunctor interface {
  7. Fmap(fn func(int) int) IntSliceFunctor // 注意!
  8. }
  9. type intSliceFunctorImpl struct {
  10. ints []int
  11. }
  12. func (isf intSliceFunctorImpl) Fmap(fn func(int) int) IntSliceFunctor {
  13. newInts := make([]int, len(isf.ints))
  14. for i, elt := range isf.ints { // 将原有元素应用 fn, 新元素集合成为新的 IntSliceFunctor
  15. retInt := fn(elt)
  16. newInts[i] = retInt
  17. }
  18. return intSliceFunctorImpl{ints: newInts}
  19. }
  20. func NewIntSliceFunctor(slice []int) IntSliceFunctor {
  21. return intSliceFunctorImpl{ints: slice}
  22. }
  23. func main() {
  24. // 原切片
  25. intSlice := []int{1, 2, 3, 4}
  26. fmt.Printf("init a functor from int slice: %#v\n", intSlice)
  27. f := NewIntSliceFunctor(intSlice)
  28. fmt.Printf("original functor: %+v\n", f)
  29. mapperFunc1 := func(i int) int {
  30. return i + 10
  31. }
  32. mapped1 := f.Fmap(mapperFunc1)
  33. fmt.Printf("mapped functor1: %+v\n", mapped1)
  34. mapperFunc2 := func(i int) int {
  35. return i * 3
  36. }
  37. mapped2 := mapped1.Fmap(mapperFunc2)
  38. fmt.Printf("mapped functor2: %+v\n", mapped2)
  39. fmt.Printf("original functor: %+v\n", f) // 原functor没有改变
  40. fmt.Printf("composite functor: %+v\n", f.Fmap(mapperFunc1).Fmap(mapperFunc2))
  41. }

运行这段代码:

  1. $ go run function_as_first_class_citizen_5.go
  2. init a functor from int slice: []int{1, 2, 3, 4}
  3. original functor: {ints:[1 2 3 4]}
  4. mapped functor1: {ints:[11 12 13 14]}
  5. mapped functor2: {ints:[33 36 39 42]}
  6. original functor: {ints:[1 2 3 4]}
  7. composite functor: {ints:[33 36 39 42]}

functor 非常适合对容器集合元素做批量同构处理,而且代码也要比每次都对容器中的元素作循环处理要优雅简洁许多。但要想在 Go 中发挥 functor 的最大效能,还需要 Go 对泛型提供支持,否则我们就需要为每一种容器类型都实现一套对应的 Functor 机制。比如上面的示例仅支持元素类型为 int 的切片,如果元素类型换为 string 或元素类型依然为 int,但容器类型换为 map,我们还需要分别为之编写新的配套代码。

延续传递式(Continuation-passing Style)

函数式编程离不开递归,以求阶乘函数为例,我们可以轻易用递归方法写出一个实现:

  1. // function_as_first_class_citizen_6.go
  2. func factorial(n int) int {
  3. if n == 1 {
  4. return 1
  5. } else {
  6. return n * factorial(n-1)
  7. }
  8. }
  9. func main() {
  10. fmt.Printf("%d\n", factorial(5))
  11. }

在 CPS 风格中,函数是不允许有返回值的。一个函数 A 应该将其想返回的值显式传给一个 continuation 函数(一般接受一个参数),而这个 continuation 函数自身是函数 A 的一个参数。概念太过抽象,我们用一个简单的例子来说明一下:

下面得 Max 函数的功能是返回两个参数值中较大的那个值:

  1. // function_as_first_class_citizen_7.go
  2. package main
  3. import "fmt"
  4. func Max(n int, m int) int {
  5. if n > m {
  6. return n
  7. } else {
  8. return m
  9. }
  10. }
  11. func main() {
  12. fmt.Printf("%d\n", Max(5, 6))
  13. }

我们把 Max 函数看作是上面定义中的 A 函数在未 CPS 化之前的状态。接下来,我们来根据 CPS 的定义将其转换为 CPS 风格:

  • 首先我们去掉 Max 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 就是定义中的 continuation 函数)
  1. func Max(n int, m int, f func(int))
  • 将返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用
  1. func Max(n int, m int, f func(int)) {
  2. if n > m {
  3. f(n)
  4. } else {
  5. f(m)
  6. }
  7. }

完整的转换后的代码如下:

  1. // function_as_first_class_citizen_8.go
  2. package main
  3. import "fmt"
  4. func Max(n int, m int, f func(y int)) {
  5. if n > m {
  6. f(n)
  7. } else {
  8. f(m)
  9. }
  10. }
  11. func main() {
  12. Max(5, 6, func(y int) { fmt.Printf("%d\n", y) })
  13. }

接下来,我们使用同样的方法将上面的阶乘实现转换为 CPS 风格。

  • 首先我们去掉 factorial 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 也就是 CPS 定义中的 continuation 函数)
  1. func factorial(n int, f func(y int))
  • 接下来,将 factorial 实现中的返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用
  1. func factorial(n int, f func(int)) {
  2. if n == 1 {
  3. f(n)
  4. } else {
  5. factorial(n-1, func(y int) { f(n * y) })
  6. }
  7. }

转换为 CPS 风格的阶乘函数的完整代码如下:

  1. / function_as_first_class_citizen_9.go
  2. package main
  3. import "fmt"
  4. func factorial(n int, f func(int)) {
  5. if n == 1 {
  6. f(1) //基本情况
  7. } else {
  8. factorial(n-1, func(y int) { f(n * y) })
  9. }
  10. }
  11. func main() {
  12. factorial(5, func(y int) { fmt.Printf("%d\n", y) })
  13. }