跳转至

函数:Go 代码的积木,以及它的七种形态

这一章讲 Go 的函数能玩出什么花样。 别的语言里函数只是「起个名字的一段代码」, Go 里函数还是一种值——能赋给变量、能当参数、能当返回值。 读完你会拿到七个具体的东西:多返回值、命名返回值、裸返回、函数类型、 匿名函数、闭包、defer、变参——以及一个主线对比:传值和传引用,Go 默认站哪边。

1. 顶层全景:函数在 Go 里是什么

书里给的定义:函数是一组完成特定任务、产出结果的语句;用函数是为了复用—— 写好一次,换着输入反复用1。

Go 程序的启动方式比书里任何一节都重要,先钉死:

  程序启动
    → 运行 main 包里的 init() 函数(有的话,只跑一次)
    → 运行 main 包里的 main() 函数(入口,无参数、无返回值)
    → main 返回,程序结束

书里给的输出顺序是:initialization of main function 在前, Program execution begins 在后2——init 是 main 的「开场白」, 适合做全程只需一次的初始化(打开连接、装配置)。

声明一个函数,第一行叫签名(signature),包含四样: func 关键字、函数名、参数列表(可以没有)、返回值列表(可以没有), 然后是一对花括号包住函数体3:

func sum(a int, b int) int {
    sum_value := a + b
    return sum_value
}

同类型的相邻参数可以合并写:func sum(a, b int)——类型写在最后一个参数后面, 前面的共享它4。

2. 多返回值:Go 最不像 C 的地方

一个 Go 函数可以同时返回好几个值,每个都有自己的类型, 返回值列表用括号 + 逗号5:

func sum(a, b int, c string) (int, string) {
    sum_value := a + b
    return sum_value, c        // 两个值一起返回
}

调用处要几个接几个:result, str := sum(a, b, "Hello Go") → 120 Hello Go6。

这个设计不是炫技,它是 Go 错误处理的物理基础—— 函数把正常结果和 error 并排返回,调用方永远见得到错误(第 17 章展开)。 在这里先记住形状:Go 的函数「最后一个返回值经常是 error」, 就是从多返回值这个能力长出来的惯例。

只想要一部分?空白标识符第三次登场

多返回值里有不想要的,用 _ 接住扔掉:result, _ := sum(a, b, "Hello Go")7。 (不让扔?Go 不许有没用的变量,这个洁癖前面已经见过两次了。)

3. 主线例子:传值 vs 传引用,一对 100 和 200 的旅程

这是本章的主线例子,原书用 a = 100、b = 200 把两种方式各跑了一遍, 我们把两条线并排走8:

  传值(call by value):call_by_value(a, b)
    函数收到的是 a、b 的复制品( Copies )
    函数里 x 改成 80,y 改成 170     ← 只动了副本
    函数返回后:a 还是 100,b 还是 200  ← 外面的原值纹丝不动

  传引用(call by reference):call_by_reference(&a, &b)
    函数收到的是 a、b 的地址(门牌号)
    函数里顺着地址把内存里的值改成 80、170
    函数返回后:a 变成 80,b 变成 170  ← 外面的原值被改了

图说:默认传值——函数怎么折腾都伤不到外面;传引用——函数直接进屋改东西。

Go 的立场写在默认值里:所有参数默认传值,想传引用必须显式写。 显式的写法是两个符号配合:&a 表示「把 a 的地址给我」, 函数那边的参数要写成指针类型 x *int,函数里用 *x 表示「顺着这个地址取值/改值」8。 (指针的完整故事是第 10 章;这里只需要知道 & 是「取门牌号」、* 是「按门牌号进屋」。)

判断(我们的,不是书里的): 「默认传值」是 Go 安全性账本的又一笔: 和「不同整数不许隐式互换」「溢出不给警告」放在一起看,会发现一个模式—— Go 允许危险的事,但每一步都要你签字。想改外面的变量?可以,写出 & 来。 如果错,会错在: 传值对大结构体(比如含几千个字段的 struct)有复制成本, 性能敏感的代码会普遍传指针——这时「签字」退化成形式主义,默认的「安全」 反而成了性能负担。这是真实的张力,不是书里的重点。

4. 命名返回值与裸返回:签名里就给结果起名

返回值可以在签名里直接起名字,起完名,函数体里就是普通变量:

func sum(a, b int, c string) (sum_value int, c_val string) {
    sum_value = a + b   // 直接用,不用 :=(它们已经声明过了)
    c_val = c
    return sum_value, c_val
}

更进一步:裸返回(naked return)——return 后面什么都不写, 按签名上名字的顺序把当前值全部带回去9:

func sum(a, b int, c string) (sum_value int, c_val string) {
    sum_value = a + b
    c_val = c
    return            // 裸返回:等价于 return sum_value, c_val
}

书里对裸返回的评价很克制:「好用,但长函数里没必要」9。 我们的补充:裸返回在小函数里是享受,在两百行的函数里是灾难—— 读的人要翻到签名才知道返回了什么。短函数裸返回,长函数写全。

5. 函数是一种类型:赋值、传递、返回

这是本章观念上最重要的一跳:在 Go 里,函数和整数、字符串一样,是一种类型。

函数类型长这样:type sub func(a int, b int) int——读作 「sub 是『吃两个 int、吐一个 int』的函数类型」10。 声明了类型之后,可以把一个符合这个形状的函数赋给变量、再调用:

var result sub
result = func(a int, b int) int { return a - b }
fmt.Println(result(100, 70))   // → 30

result 现在是一个变量,但括号一跟,就是函数调用10。

匿名函数:不取名字的函数

没有名字的函数叫匿名函数(anonymous function,别的语言也叫 lambda)。 两种用法:

a := func() { fmt.Println("Introducing anonymous function") }
a()                    // 赋给变量再调用

func() { fmt.Println("Introducing anonymous function") }()
                       // 定义完当场调用(结尾多一对括号)

两种写法的输出一样11。当场调用那种写法,在并发一章会变身成 「开一个 goroutine 就跑这段」的标准姿势。

函数返回函数

既然函数是值,函数当然能返回函数:

func return_anon() func(string) {        // 返回值本身是一个函数类型
    return func(message string) {
        fmt.Println(message)
    }
}

f := return_anon()   // f 现在是那个匿名函数
f("Hello")           // → Hello

书里特别提示:打印 f 本身,看到的是函数的地址(形如 0x47d880)—— 函数在内存里也是个有门牌号的东西12。

6. 闭包:函数带着它的「私有行李」

闭包(closure)是一个引用了自己函数体之外变量的函数;它出门的时候, 把那个变量一起打包带走——而且带走的是「活的」绑定,不是复印件。13

这是全书最适合逐行走一遍的例子,原书的计数器:

func return_anon() func() int {
    count := 0
    return func() int {     // 这个匿名函数引用了外层的 count
        count++
        return count
    }
}

counter := return_anon()
fmt.Println(counter())   // → 1
fmt.Println(counter())   // → 2   ← count 被记住了!

counter2 := return_anon()  // 再要一个新的
fmt.Println(counter2())  // → 1   ← 各记各的账

图说:每次调用 return_anon(),都会新造一个 count,并且把匿名函数和这个 count 绑在一起寄出去。counter 和 counter2 带走的是两个互不相干的 count。 书里点明了关键:「引用函数体之外的变量」就是闭包的定义本身13。

闭包是 Go 里「函数 + 它周围的数据」打包复用的最小单元, 以后看到「选项模式」「中间件」这类高级玩法,底下都是它。

7. 递归:函数调用自己

递归就是一个函数直接或间接调用自己,并且必须有终止条件, 否则就是无限递归——栈被一层层压满,程序崩掉14。

书里给的教科书例子,阶乘(num > 1 时 num × factorial(num-1), num ≤ 1 时返回 1):factorial(10) → 362880015。 递归的两件套:往简单缩(问题每步变小)+ 停得下来(有终止条件), 缺一件都是事故。

8. defer:「走之前记得关火」

defer 的意思是:这个函数调用先记账,等外层函数(比如 main)快退出时再执行。 如果写了多个 defer,执行顺序是后进先出(LIFO)——最后记的账最先还16:

func main() {
    fmt.Println("Before defer statement")
    defer defer_fun()                       // 记账
    fmt.Println("After defer statement")    // 正常执行
}
// 输出顺序:
//   Before defer statement
//   After defer statement
//   Hi I am introducing deferred function   ← main 退出时才轮到它

图说:defer 像出门前贴在门上的便签——「走之前关火」。 便签一张压一张,执行时从最后一张往前做16。

两个细节书里明确给了:

  • defer 那一行的参数,在记账那一刻就算好了;真正执行的是「函数调用」本身16;
  • 用途:关文件、还锁、收尾清理——第 17 章会看到它和错误处理、panic/recover 组成 Go 的完整善后体系。现在记住一句:打开资源(文件、锁、网络连接这类 占着就得还的东西)的下一行就写 defer 关它, 永远不会忘。

9. 变参函数:参数个数随便

变参(variadic)函数接受任意个数的同类型参数,写法是在最后一个参数的类型前 加 ...:args ...int 念作「零个或多个 int」,函数体内它就是个切片17:

func add(args ...int) int {
    total := 0
    for _, v := range args {
        total += v
    }
    return total
}
add(1, 2, 3)   // → 6
add()          // 也行,args 是空切片,返回 0

... 只能出现在最后一个参数上;已经握在手里的切片想拆开来传,用 slice... (补充,不在书里,来自通用知识)。

10. 作者的判断与证据

这一章同样是规则为主。带判断色彩的话有两处:

书里的说法 性质
「裸返回好用,长函数里不必要」 作者经验之谈,属实且克制
「闭包能记住外部变量的值」 机制事实,例子可跑验

书中示例代码有个别排版瑕疵(如把 sum_value := a+b return sum_value, c 挤在一行),逻辑不受影响,照机制读即可。

11. 边界与局限

  • 方法(method)没有出现:给 struct 绑函数的「方法」是第 11–13 章的内容, 本章的函数都是「自由函数」;
  • defer 一节只给了顺序和语法,没讲 defer + return 值的经典坑 (defer 里改命名返回值会改变最终返回)——用到时留意(补充,不在书里);
  • 变参只演示了 int,实际上 ... 对任意类型可用;
  • 没有讲「高阶函数」这个名目,但函数作参数、作返回值的机制已经全部给齐, 第 13 章接口会再见到它们。

12. 可带走的

  1. 启动顺序:init() → main() → 结束;init 只跑一次,做一次性初始化;
  2. 签名 = func + 名 + 参数 + 返回值;同类型参数可合并写;
  3. Go 函数可以一次返回多个值——错误处理的物理基础;
  4. 只想要部分返回值,用 _ 丢弃;
  5. 默认传值:函数收到的是复制品;想改外面的变量,传 &地址, 参数写成指针,函数里用 * 进屋;
  6. 返回值可以起名;裸返回(return 不带值)按签名顺序带回——短函数可用,长函数别用;
  7. 函数是类型:能赋给变量、能当参数、能当返回值;没有名字的函数直接调用,是 goroutine 的雏形;
  8. 闭包 = 引用外部变量的函数,它把那个变量活着打包带走;每次「造」一个就有一份新数据;
  9. 递归两件套:问题每步变小 + 有终止条件;
  10. defer 记账、退出时还:后进先出;参数记账时就算好;用于关文件、还锁、善后。

13. 原文地图

主题 原书章 原文位置
函数的作用 Functions text/14-fm-learning-go-programming.txt:73(搜「reusability」)
init 先于 main Functions text/14-fm-learning-go-programming.txt:76(搜「entrance door」) · text/14-fm-learning-go-programming.txt:109(搜「initialization of main function」)
函数签名与语法 Functions text/14-fm-learning-go-programming.txt:118(搜「signature of the function」)
参数合并写、多返回值 Functions text/14-fm-learning-go-programming.txt:196(搜「avoid repetition」) · text/14-fm-learning-go-programming.txt:235(搜「returning multiple values」)
调用函数 Functions text/14-fm-learning-go-programming.txt:259(搜「control is transferred」)
传值 Functions text/14-fm-learning-go-programming.txt:316(搜「do not reflect」) · text/14-fm-learning-go-programming.txt:337(搜「In function, value of a」)
传引用 Functions text/14-fm-learning-go-programming.txt:409(搜「address of a variable」) · text/14-fm-learning-go-programming.txt:481(搜「After calling function, value of a」)
命名返回值与裸返回 Functions text/14-fm-learning-go-programming.txt:490(搜「name values」) · text/14-fm-learning-go-programming.txt:508(搜「naked return」)
多返回值 + 空白标识符 Functions text/14-fm-learning-go-programming.txt:565(搜「blank identifiers are used」)
函数类型 Functions text/14-fm-learning-go-programming.txt:622(搜「create our function type」)
匿名函数 Functions text/14-fm-learning-go-programming.txt:676(搜「lambda or anonymous」)
返回函数 Functions text/14-fm-learning-go-programming.txt:748(搜「return an anonymous function」)
闭包与计数器 Functions text/14-fm-learning-go-programming.txt:802(搜「out of its body」) · text/14-fm-learning-go-programming.txt:868(搜「count is: 2」)
递归与阶乘 Functions text/14-fm-learning-go-programming.txt:880(搜「termination point」) · text/14-fm-learning-go-programming.txt:943(搜「3628800」)
defer 与后进先出 Functions text/14-fm-learning-go-programming.txt:949(搜「Last In First Out」) · text/14-fm-learning-go-programming.txt:991(搜「deferred function」)
变参函数 Functions text/14-fm-learning-go-programming.txt:997(搜「arbitrary number of arguments」) · text/14-fm-learning-go-programming.txt:1033(搜「6」)

  1. 出处:「Functions」第 73 段(text/14-fm-learning-go-programming.txt:73,搜「reusability」)。 ↩

  2. 出处:「Functions」第 76、109 段(text/14-fm-learning-go-programming.txt:76,搜「entrance door」;text/14-fm-learning-go-programming.txt:109,搜「initialization of main function」)。 ↩

  3. 出处:「Functions」第 118、124 段(text/14-fm-learning-go-programming.txt:118,搜「signature of the function」)。 ↩

  4. 出处:「Functions」第 196–217 段(text/14-fm-learning-go-programming.txt:196,搜「avoid repetition」)。 ↩

  5. 出处:「Functions」第 235 段(text/14-fm-learning-go-programming.txt:235,搜「returning multiple values」)。 ↩

  6. 出处:「Functions」第 295–307 段(text/14-fm-learning-go-programming.txt:307,搜「Values returned from the function are」)。 ↩

  7. 出处:「Functions」第 565–616 段(text/14-fm-learning-go-programming.txt:604,搜「result, _」)。 ↩

  8. 出处:「Functions」第 316–340 段(text/14-fm-learning-go-programming.txt:316,搜「do not reflect」;text/14-fm-learning-go-programming.txt:337,搜「In function, value of a」)与第 409–484 段(text/14-fm-learning-go-programming.txt:409,搜「address of a variable」;text/14-fm-learning-go-programming.txt:481,搜「After calling function, value of a」)。两组输出照原书。 ↩↩

  9. 出处:「Functions」第 490、508 段(text/14-fm-learning-go-programming.txt:490,搜「name values」;text/14-fm-learning-go-programming.txt:508,搜「naked return」)。 ↩↩

  10. 出处:「Functions」第 622–670 段(text/14-fm-learning-go-programming.txt:622,搜「create our function type」;text/14-fm-learning-go-programming.txt:670,搜「Result is」)。 ↩↩

  11. 出处:「Functions」第 676–742 段(text/14-fm-learning-go-programming.txt:676,搜「lambda or anonymous」;text/14-fm-learning-go-programming.txt:742,搜「same output」)。 ↩

  12. 出处:「Functions」第 748–796 段(text/14-fm-learning-go-programming.txt:778,搜「address of function」;text/14-fm-learning-go-programming.txt:793,搜「Hello」)。 ↩

  13. 出处:「Functions」第 802–874 段(text/14-fm-learning-go-programming.txt:802,搜「out of its body」;text/14-fm-learning-go-programming.txt:868,搜「count is: 2」;text/14-fm-learning-go-programming.txt:874,搜「key point in closures」)。 ↩↩

  14. 出处:「Functions」第 880–895 段(text/14-fm-learning-go-programming.txt:880,搜「termination point」)。 ↩

  15. 出处:「Functions」第 907–943 段(text/14-fm-learning-go-programming.txt:943,搜「3628800」)。 ↩

  16. 出处:「Functions」第 949–991 段(text/14-fm-learning-go-programming.txt:949,搜「Last In First Out」;text/14-fm-learning-go-programming.txt:988,搜「Before defer statement」)。原文明确:defer 语句的参数在 defer 时求值,函数执行被推迟。 ↩↩↩

  17. 出处:「Functions」第 997–1036 段(text/14-fm-learning-go-programming.txt:997,搜「arbitrary number of arguments」;text/14-fm-learning-go-programming.txt:1033,搜「6」)。 ↩