让自己习惯函数是一等公民

65 阅读4分钟

让自己习惯函数是一等公民

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

​ ——Ward Cunningham

在Go中函数可以正常创建

func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

可以在函数中创建函数

func hexdumpWords(p, end uintptr, mark func(uintptr) byte) {
    p1 := func(x uintptr) {
        var buf [2 * sys.PtrSize]byte
        for 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.go
type HandlerFunc func(ResponseWriter, *Request)

// $GOROOT/src/sort/genzfunc.go
type 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.go
func vdsoParseSymbols(info *vdsoInfo, version int32) {
    ....
    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.go

func 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.go
func 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

函数作为一等公民的特殊运用

像对整型变量那样对函数进行显式类型转换

Go是类型安全的语言,不接受隐式类型转换

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

必须进行显式类型转换才能通过编译器的检查

var a int = 5
var b int32 = 6
fmt.Println(a + int(b)) // 正确:输出11

这种转换在函数中也可以实现,最典型的例子是http.HandlerFunc这个类型

func greeting(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome, Gopher!\n")
}

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

http.ListenAndServe()函数接受两个参数,第二个为Handler接口,需要实现ServeHTTP(ResponseWriter,*Resquest)方法
我们的函数greeting虽然参数一样单函数名不同直接放进去的话就会产生“func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)” 这个错误,于是创建一个类型

type HandlerFunc func(w ResponseWriter,r *Request)
// ServeHTTP调用f(w, r)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
这样HandlerFunc则实现了Handler接口,其实就是一个基于函数定义的新类型,底层类型为func(w ResponseWriter, r *Request)
也就是说这个类型将函数greeting显式转换成HandlerFunc类型,后者实现了Handler则满足了ListenAndServe第二个参数的要求

函数式编程

柯里化函数

柯里化是吧接受多个参数的函数变成接受一个单一参数(原函数的第一个参数)的函数,并返回接受余下的参数和返回结果的新函数的技术。

package main

import "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))
}

这个例子利用了函数的两点性质:在函数内定义和闭包“闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。本质上,闭包是将函数内部和函数外部连接起来的桥梁。 以上述示例来说,partialTimes内部定义的匿名函数就是一个闭包,该匿名函数访问了其外部函数partialTimes的变量x。

函子:略

延续传递式:略

小结

成为“一等公民”的函数极大增强了Go语言的表现力,我们可以对待值变量那样对待函数,上述函数编程思想的运用就得益于此

让自己习惯于函数是一等公民

  • Go函数可以像变量值那样被赋值给变量、作为参数传递、作为返回值返回和在函数内部创建等;
  • 函数可以像变量那样被显式类型转换;
  • 基于函数特质,了解Go中的几种有用的函数式编程风格,如柯里化、函子等;
  • 不要为了符合特定风格而滥用函数特质。”