函数定义

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

  1. func name(parameter-list) (result-list) {
  2. body
  3. }
  4. //如果参数列表中若干个相邻的参数类型的相同,则可以在参数列表中省略前面变量的类型声明,如下所示:
  5. func Add(a, b int)(ret int, err error) {
  6. //...
  7. }
  8. //如果函数只有一个返回值,也可以这么写:
  9. func Add(a, b int) int {
  10. // ...
  11. }

不定参数

  1. func myfunc(args ...int) {
  2. for _, arg := range args {
  3. fmt.Println(arg)
  4. }
  5. }
  6. myfunc(2, 3, 4)
  7. myfunc(1, 3, 7, 13)
  8. //不定参数的传递
  9. func myfunc(args ...int) {
  10. // 按原样传递
  11. myfunc3(args...)
  12. // 传递片段,实际上任意的int slice都可以传进去
  13. myfunc3(args[1:]...)
  14. }
  15. //任意类型的不定参数
  16. func Printf(format string, args ...interface{}) {
  17. // ...
  18. }

多返回值

image.png
golang函数支持多个返回值,如果调用方调用了一个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单地用一个下划线“”来跳过这个返回值:
n,
:= f.Read(buf)
如果没有指定某个返回值的具体值,则返回返回值类型的零值。
相较于传统C中的callee-save模式,go编译器采用的是caller-save模式,即,由调用者负责保存寄存器。
image.png

函数值和闭包

函数值

在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。例子如下:

  1. func square(n int) int { return n * n }
  2. func negative(n int) int { return -n }
  3. func product(m, n int) int { return m * n }
  4. f := square
  5. fmt.Println(f(3)) // "9"
  6. f = negative
  7. fmt.Println(f(3)) // "-3"
  8. fmt.Printf("%T\n", f) // "func(int) int"
  9. f = product // compile error: can't assign func(int, int) int to func(int) int
  10. //这是一个匿名函数,匿名函数由一个不带函数名的函数声明和函数体组成
  11. f := func(x, y int) int {
  12. return x + y
  13. }
  14. func(ch chan int) {
  15. ch <- ACK
  16. } (reply_chan) // 花括号后直接跟参数列表表示函数调用

函数类型的零值是nil。调用值为nil的函数值会引起panic错误:

  1. var f func(int) int
  2. f(3) // 此处f的值为nil, 会引起panic错误

函数值可以与nil比较:

  1. var f func(int) int
  2. if f != nil {
  3. f(3)
  4. }

但是函数值之间是不可比较的,也不能用函数值作为map的key。

闭包

概念:闭包是匿名函数与匿名函数所引用环境的组合。匿名函数有动态创建的特性,该特性使得匿名函数不用通过参数传递的方式,就可以直接引用外部的变量。 闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。
image.png
价值:闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
变量a指向的闭包函数引用了局部变量i和j,i的值被隔离,在闭包外不能被修改,改变j的值以后,再次调用a,发现结果是修改过的值。在变量a指向的闭包函数中,只有内部的匿名函数才能访问变量i,而无法通过其他途径访问到,因此保证了i的安全性。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. var j int = 5
  7. a := func()(func()) {
  8. var i int = 10
  9. return func() {
  10. fmt.Printf("i, j: %d, %d\n", i, j)
  11. }
  12. }()
  13. a()
  14. j *= 2
  15. a()
  16. }

上述例子的执行结果是:
i, j: 10, 5
i, j: 10, 10
在上面的例子中,变量a指向的闭包函数引用了局部变量i和j,i的值被隔离,在闭包外不能被修改,改变j的值以后,再次调用a,发现结果是修改过的值。
在变量a指向的闭包函数中,只有内部的匿名函数才能访问变量i,而无法通过其他途径访问到,因此保证了i的安全性。

  • 例子

image.png

  • 逃逸分析

image.png

defer

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。

  • 当defer被声明时,其参数就会被实时解析
  • defer执行顺序为先进后出
  • defer可以读取有名返回值

defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

错误处理

Panic异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。
一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。
不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径:

  1. switch s := suit(drawCard()); s {
  2. case "Spades": // ...
  3. case "Hearts": // ...
  4. case "Diamonds": // ...
  5. case "Clubs": // ...
  6. default:
  7. panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
  8. }

注:在Go的panic机制中,延迟函数的调用在释放堆栈信息之前,可以利用这个特点在退出时输出异常信息

Recover捕获异常

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
deferred函数帮助Parse从panic中恢复。在deferred函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。
不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。

假如我们对于foo()函数的执行要么心里没底感觉可能会触发错误处理,或者自己在其中明确加入了按特定条件触发错误处理的语句,那么可以用如下方式在调用代码中截取recover():

  1. defer func() {
  2. if r := recover(); r != nil {
  3. log.Printf("Runtime error caught: %v", r)
  4. }
  5. }()
  6. foo()

无论foo()中是否触发了错误处理流程,该匿名defer函数都将在函数退出时得到执行。假如foo()中触发了错误处理流程,recover()函数执行将使得该错误处理过程终止。如果错误处理流程被触发时,程序传给panic函数的参数不为nil,则该函数还会打印详细的错误信息。