函数声明

函数声明的格式:

1
2
3
func name(parameter-list) (result-list) {
    body
}
  • 如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的
  • 如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值
  • 参数
    • Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
    • 在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
    • 实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,实参可能会由于函数的间接引用被修改。
  • 函数签名
    • 不定义函数体即可
    • 例如:func Sin(x float64) float //implemented in assembly language

递归

多返回值

  • func Size(rect image.Rectangle) (width, height int)
  • bare return
    • 函数声明是声明多个返回值,在函数体中return时可以只写return,默认返回声明中的变量
    • 使用bare return可以不用在return时再次声明返回值(但这样容易出错)

错误

  • 内置的error是接口类型
  • 打印error可以获得稍微详细一点的错误信息
  • go error设计哲学
    • 在Go中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常(exception)
    • 虽然Go有各种异常机制,但这些机制仅被使用在处理那些未被预料到的错误,即bug,而不是那些在健壮程序中应该被避免的程序错误。
    • Go使用控制流机制(如if和return)处理错误,这使得编码人员能更多的关注错误处理

函数值

  • 在go中,函数被看作是第一类值(first-class values),函数拥有类型,可以被赋值给其他变量,传递给函数,从函数返回,例如
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
    if pre != nil {
        pre(n)
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        forEachNode(c, pre, post)
    }
    if post != nil {
        post(n)
    }
}
  • 函数的零值是nil
  • 函数之间不可以比较,不能使用函数作为map的key

匿名函数

  • 匿名函数与普通的函数相比,只是少了函数名
  • 函数值不可比较的一个原因是匿名函数(考虑这样的情景:在闭包中,匿名函数引用了一个闭包内匿名函数外的变量,这个变量可变)
  • 闭包老毛病:延迟绑定或者说变量变化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var rmdirs []func()
for _, d := range tempDirs() {
    dir := d // NOTE: necessary!
    os.MkdirAll(dir, 0755) // creates parent directories too
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}
// ...do some work…
for _, rmdir := range rmdirs {
    rmdir() // clean up
}

可变参数

使用...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

func sum(vals ... int)int{
	var total int = 0
	for _, val := range vals{
		total += val
	}
	return total
}

func main(){
	println(sum())
	println(sum(1, 2))
	println(sum(1,2,3))
}

  • 可变参数和slice的区别?(我感受不到区别)
1
2
3
4
func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"
  • 如何将一个slice作为参数传递给上面例子中的vals? sum(slice...)即可

defer

  • 在资源获取的地方使用defer函数处理资源释放
  • 利用defer可以在资源释放后实现很多奇奇怪怪的操作
  • 注意事项
    • 在循环中使用defer,例如在循环中打开文件,并使用defer声明关闭操作,那么需要所有文件打开并操作完成后,才会调用每一个defer语句(容易造成文件描述符用尽)
    • 解决方法:循环里调用函数,在函数里进行defer close()操作,函数在结束时会立马执行defer操作
  • 其他
    • 许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功

panic异常

  • 当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)
  • 程序崩溃并产生日志,日志信息包括panic value函数调用的堆栈跟踪信息
  • 直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。
  • panic一般用于严重错误(go提倡对于大部分漏洞,使用Go提供的错误机制而不是panic)
  • runtime包允许程序员输出堆栈信息
1
2
3
4
5
6
7
8
9
func main() {
    defer printStack()
    f(3)
}
func printStack() {
    var buf [4096]byte
    n := runtime.Stack(buf[:], false)
    os.Stdout.Write(buf[:n])
}
  • 在Go的panic机制中,延迟函数的调用在释放堆栈信息之前

recover捕获异常

  • 通常我们不应该对panic做出任何处理,但有时候或许我们可以在程序崩溃前做一些操作
    • 例如服务器遇到不可预料的问题时,可以在崩溃前关闭连接,如果不关闭,客户端会一直处于等待的状态
  • 如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil
1
2
3
4
5
6
7
8
func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
    // ...parser...
}
  • panic的处理方法
    • 不推荐的做法
      • 不加区分的恢复所有的panic异常,不是可取的做法;作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic;
    • 推荐做法
      • 在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为error处理,如果不是,则按照正常的panic进行处理
      • 其中panic value的value是通过panic(value)抛出的