《Go语言精进之路》读书笔记 | 让自己习惯于函数是“一等公民”

Posted COCOgsta

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go语言精进之路》读书笔记 | 让自己习惯于函数是“一等公民”相关的知识,希望对你有一定的参考价值。

书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》

一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:《Go语言精进之路》读书笔记 | 汇总_COCOgsta的博客-CSDN博客


Go程序就是一组函数的集合。函数在Go语言中属于“一等公民”。

21.1 什么是“一等公民”

引用一下wiki发明人、C2站点作者Ward Cunningham对“一等公民”的诠释:

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

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

(1)正常创建

我们可以在源码顶层正常创建一个函数:

// $GOROOT/src/fmt/print.go
func newPrinter() *pp 
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p

复制代码

(2)在函数内创建

可以在函数内定义一个新函数,如下面代码中在hexdumpWords函数内部定义的匿名函数(被赋值给变量p1)。

// $GOROOT/src/runtime/print.go
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[:])
    
    ...

复制代码

(3)作为类型

可以使用函数来自定义类型,如下面代码中的HandlerFunc、visitFunc和action:

// $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)
复制代码

(4)存储到变量中

可以将定义好的函数存储到一个变量中,如下面代码中的apply:

// $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
    
    ...

复制代码

(5)作为参数传入函数

可以将函数作为参数传入函数,比如下面代码中函数AfterFunc的参数f:

// $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

复制代码

(6)作为返回值从函数返回

函数还可以被作为返回值从函数返回,如下面代码中函数makeCutsetFunc的返回值就是一个函数:

// $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 

复制代码

正如Ward Cunningham对“一等公民”的诠释,Go中的函数可以像普通整型值那样被创建和使用。

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

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

对整型变量进行的操作也可以用在函数上,即函数也可以被显式类型转换,并且这样的类型转换在特定的领域具有奇妙的作用。

// chapter4/sources/function_as_first_class_citizen_2.go

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


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

复制代码

上述代码是最为常见的一个用Go构建的Web Server的例子。其工作机制很简单,当用户通过浏览器或类似curl这样的命令行工具访问Web Server的8080端口时,会收到“Welcome, Gopher!”这行文字版应答。

  1. 函数式编程

虽然Go不推崇函数式编程,但有些时候局部应用函数式编程风格可以写出更优雅、更简洁、更易维护的代码。

(1)柯里化函数

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

来看一个用Go编写的柯里化函数的例子:

// chapter4/sources/function_as_first_class_citizen_4.go
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))

复制代码

运行这个例子:

$ go run function_as_first_class_citizen_4.go
10
15
20
复制代码

这里的柯里化是指将原来接受两个参数的函数times转换为接受一个参数的函数partialTimes的过程。通过partialTimes函数构造的timesTwo将输入参数扩大为原先的2倍、timesThree将输入参数扩大为原先的3倍,以此类堆。

这个例子利用了函数的两点性质:在函数中定义,通过返回值返回;闭包。

闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。

上述示例,partialTimes内部定义的匿名函数就是一个闭包,该匿名函数访问了其外部函数partialTimes的变量x。这样当调用partialTimes(2)时,partialTimes实际上返回一个调用times(2, y)的函数:

timesTwo = func(y int) int 
    return times(2, y)

复制代码

(2)函子

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

函子本身是一个容器类型,以Go语言为例,这个容器可以是切片、map甚至channel; 该容器类型需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新函子,原函子容器内部的元素值不受影响。

函子非常适合用来对容器集合元素进行批量同构处理,而且代码也比每次都对容器中的元素进行循环处理要优雅、简洁许多。但要想在Go中发挥函子的最大效能,还需要Go对泛型提供支持,否则我们就需要为每一种容器类型都实现一套对应的Functor机制。

(3)延续传递式

函数式编程有一种被称为延续传递式(Continuation-passing Style,CPS)的编程风格可以充分运用函数作为“一等公民”的特质。

在CPS风格中,函数是不允许有返回值的。一个函数A应该将其想返回的值显式传给一个continuation函数(一般接受一个参数),而这个continuation函数自身是函数A的一个参数。

这种CPS风格虽然利用了函数作为“一等公民”的特质,但是其代码理解起来颇为困难,这种风格真的好吗?选择了不适合的风格或者为了函数式而进行函数式编程,那么就会出现代码难于理解且代码执行效率不高的情况。

以上是关于《Go语言精进之路》读书笔记 | 让自己习惯于函数是“一等公民”的主要内容,如果未能解决你的问题,请参考以下文章

《Go语言精进之路》读书笔记 | 理解Go语言的设计哲学

《Go语言精进之路》读书笔记 | 了解Go语言的诞生与演进

《Go语言精进之路》读书笔记 | 在init函数中检查包级变量的初始状态

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码

《Go语言精进之路》读书笔记 | 汇总