Go语言圣经 .4.函数

函数

函数声明

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

func name(parameter-list) (result-list) {
body
}

下面是几种声明示例:

func add(x int, y int) int { return x + y }
func sub(x, y int) (z int) { z = x -y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }

函数声明上的参数称作形参,传入函数的参数称作实参。

  • 形参:都是函数的局部变量,初始值由调用者提供的实参传递
  • 实参:按值传递,函数接受到的是实参的副本,所以修改函数的形参并不会影响到实参。但是如果提供的实参包含引用类型,比如指针、slice、map、函数或者通道,那么函数使用形参变量时就有可能会间接的修改实参。

递归

许多编程语言使用固定长度大小的函数调用栈,大小在64kb到2mb之间。递归的深度会受限于固定长度的栈大小,所以当进行深度递归调用的时候必须谨防栈溢出。GO语言实现了可变长度的栈,栈的大小会随着使用而增长,可达到1GB左右的上限。

多返回值

习惯上最后一个布尔返回值表示成功与否

一个函数如果有命名的返回值,可以省略return语句的操作数,这称作裸返回

例如:

func CountWordsAndImages(url string) (words, images int, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
doc, err := http.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return
}
words, images = countWordsAndImages(doc)
return
}

错误

错误处理策略

  1. 传递下去

  2. 超出一定重试次数和限定的时间后报错退出

  3. 停止程序

  4. 只记录错误信息,程序继续运行

  5. 罕见的情况下我们可以直接安全的忽略掉整个日志

    dir, err := ioutil.TemDir("", "scrath")
    if err!=nil {
    return fmt.Errorf("fail")
    }
    // ...使用临时目录...
    os.RemoveAll(dir) // 忽略错误, $TMPDIR会被周期性删除

文件结束标识

io包保证任何由文件结束引起的读取错误,始终都会得到一个与众不同的错误--io.EOF,它的定义如下:

package io

import "errors"

var EOF = errors.New("EOF")

函数值(函数变量)

函数值就像其他值,函数变量也有类型,可以赋给变量或者传递或者从其他函数中返回。函数变量可以像其他函数一样调用。比如:

func square(n int) int { return n*n }
func negative(n int) int { return -n }
func product(m, n int) int {return m*n}

f := square
fmt.Println(f(3)) // "9"

f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"

f = product // 编译错误:无法将func(int int) int 赋给func(int) int

匿名函数

命名函数只能在包级别的作用域进行声明,但我们能够使用函数字面量在任何表达式内指定函数变量。函数字面量就像函数声明,但在func关键字后面没有函数的名称。它是一个表达式,它的值称作匿名函数。

import "strings"

strings.Map(func(r rune) rune {return r+1}, "HAL-9000") // "IBM.:111"

警告:捕获迭代变量

var rmdirs []func()
for _, d := range tempDirs() {
dir := d // 注意!!,这一行是必须的
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func(){
os.RemoveAll(dir)
})
}

// ...其他...

for _, rmdir := range rmdirs {
rmdir() // 删除
}

这里你可能会奇怪,为什么循环体内要将循环变量赋值给一个新的余部变量dir,而不是直接append循环变量d。

这是错误示范:

var rmdirs []func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func(){
os.RemoveAll(dir) // 错误!!!
})
}

这个原因是循环变量的作用域的规则限制。dir在for循环引进的一个块作用域内进行声明。在循环里创建的所有函数变量共享相同得到变量——一个可访问的存储位置,而不是固定的值。dir变量的值在不断的迭代中更新,因此,当最后调用清理函数的时候,dir变量已经被每一次的for循环更新多次。因此,dir变量的实际取值是最后一次迭代时的值。

可变参数(变长函数)

变长函数被调用的时候可以有可变的参数个数。在参数列表最后类型名称之前使用省略号“...”表示声明一个变长函数,调用这个函数的时候可以传递该类型任意数目的参数。

例如:

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

sum() // 0
sum(3) // 3
sum(1,2,3,4) // 10

values := []int{1,2,3,4}
sum(values...) // 10

Deferred函数

使用defer语句后,无论是正常执行完毕,或者宕机异常等,实际的调用都推迟到包含defer语句的函数结束后才执行。

defer语句没有限制使用次数,执行的时候按照调用defer语句顺序的倒叙执行。

func double(x int) (result int) {
defer func() { fmt.Printf("double(%d) = %d\n", x, result) }
return x + x
}

_ = double(4)
// "double(4) = 8"

Panic异常

package main

import (
"fmt"
"runtime"
"os"
)


func f(x int) {
fmt.Printf("f(%d)\n", x+0/x)
defer fmt.Printf("defer %d\n", x)
f(x - 1)
}

func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}

func main() {
defer printStack()
f(3)
}



输出:

f(3)
f(2)
f(1)
defer 1
defer 2
defer 3
goroutine 1 [running]:
E:/study/go_from_zero/ch5/defer2.go:24 +0x65
panic(0x469560, 0x513c60)
E:/scriptpath/go/src/runtime/panic.go:965 +0x1c7
main.f(0x0)
E:/study/go_from_zero/ch5/defer2.go:11 +0x1e8
main.f(0x1)
E:/study/go_from_zero/ch5/defer2.go:13 +0x189
main.f(0x2)
E:/study/go_from_zero/ch5/defer2.go:13 +0x189
main.f(0x3)
E:/study/go_from_zero/ch5/defer2.go:13 +0x189
main.main()
E:/study/go_from_zero/ch5/defer2.go:18 +0x53
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.f(0x0)
E:/study/go_from_zero/ch5/defer2.go:11 +0x1e8
main.f(0x1)
E:/study/go_from_zero/ch5/defer2.go:13 +0x189
main.f(0x2)
E:/study/go_from_zero/ch5/defer2.go:13 +0x189
main.f(0x3)
E:/study/go_from_zero/ch5/defer2.go:13 +0x189
main.main()
E:/study/go_from_zero/ch5/defer2.go:18 +0x53
exit status 2

Recover捕获异常

退出程序通常是正确处理宕机的方式,但也有例外。在一定情况下是可以进行回复的,至少有时候可以在退出前理清当前混乱的情况。

func soleTitle(doc *html.Node) (title string, err error) {
type bailout struct{}
defer func(){
switch p:= revcover(); p {
case nil:
// 没有宕机
case bailout{}:
// 预期的宕机
err = fmt.Errof("multiple title elements")
default:
// 未预期的宕机
panic(p)
}
}()
}

练习

练习 5.1

修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。

练习 5.2

编写函数,记录在HTML树中出现的同名元素的次数。

练习 5.3

编写函数输出所有text结点的内容。注意不要访问 <script> 和 <style> 元素,因为这些元素对浏览者是不可见的。

练习 5.4

扩展visit函数,使其能够处理其他类型的结点,如images、scripts和style sheets