函数声明

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

1
2
3
func function-name(param...) (result...) {
    body
}

形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。

1
2
3
4
func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // "5"

递归

函数可以是递归的,这意味着函数可以直接或间接的调用自身。对许多问题而言,递归是一种强有力的技术,例如处理递归的数据结构。

1
2
3
4
5
6
7
func a(i int) (res int){
    if i == 1 {
        return i 
    }
    return i * a(i - 1)
}
fmt.Println(a(5)) // 120

多返回值

在Go中,一个函数可以返回多个值。

1
2
3
4
func calculation(a,b int)(add,sub int) {
	add = a + b
    sub = a - b
}

错误

在Go中有一部分函数总是能成功的运行,对各种可能的输入都做了良好的处理,使得运行时几乎不会失败,除非遇到灾难性的、不可预料的情况,比如运行时的内存溢出。导致这种错误的原因很复杂,难以处理,从错误中恢复的可能性也很低。

panic是来自被调函数的信号,表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。

1
2
3
4
value, ok := cache.Lookup(key)
if !ok {
    // ...cache[key] does not exist…
}

错误处理策略

当一次函数调用返回错误时,调用者有应该选择何时的方式处理错误。根据情况的不同,有很多处理方式.

1
2
3
4
resp,err := http.Get("https://www.google.com")
if err != nil {
    fmt.Println(err)
}

文件结尾错误(EOF)

函数经常会返回多种错误,这对终端用户来说可能会很有趣,但对程序而言,这使得情况变得复杂。很多时候,程序必须根据错误类型,作出不同的响应。让我们考虑这样一个例子:从文件中读取n个字节。如果n等于文件的长度,读取过程的任何错误都表示失败。如果n小于文件的长度,调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义:

1
2
3
4
package io
import "errors"
// EOF is the error returned by Read when no more input is available.
var EOF = errors.New("EOF")

调用者只需通过简单的比较,就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符,以及判断文件结束。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
in := bufio.NewReader(os.Stdin)
for {
    r, _, err := in.ReadRune()
    if err == io.EOF {
        break // finished reading
    }
    if err != nil {
        return fmt.Errorf("read failed:%v", err)
    }
    // ...use r…
}

因为文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息——“EOF”。对于其他错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们会提出更系统的方法区分某些固定的错误值。

函数值

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

1
2
3
4
5
6
7
func add(a,b int) (sum int) {
    sum = a + b 
}
func main() {
    f = add
    fmt.Println(sum(1,2))
}

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

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

匿名函数

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}
func main() {
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
    fmt.Println(f()) // "16"
}

可变参数

参数数量可变的函数称为为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个的必备参数,之后接收任意个数的后续参数。

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
	fmt.Println(sum(1,2,3,4,5)) // 15
	var sli = []int{1,2,3,4,5}
	fmt.Println(sli)		// [1 2 3 4 5]
	fmt.Println(sum(sli...)) // 15
}
func sum(values ...int) int {
	sum := 0
	for _, v := range values {
		sum += v
	}
	return sum
}

Panic异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。

一般而言,当panic异常发生时,程序会中断运行,并执行此goroute上的defer函数。

当某些不应该发生的场景发生时,我们就应该调用panic。

1
2
3
4
5
6
7
8
9
name := "zhaohaiyu"
	switch name {
	case "zhy":
		fmt.Println("zhy")
	case "haiyuzhao":
		fmt.Println("haiyuzhao")
	default:
		panic("没有这个名字")
	}

虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。

Recover捕获异常

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

让我们以语言解析器为例,说明recover的使用场景。考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。

1
2
3
4
5
6
7
defer func ()  {
		if p := recover();  p != nil {
			fmt.Println(p)  // 主动抛错
            	// 可以进行写日志等操作
		}
	}()
	panic("主动抛错")