函数声明
函数声明包括函数名、 形式参数列表、返回值列表(可省略)以及函数体。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。
func name(parameter-list) (result-list) {
body
}
如果一组形参或返回值有相同的类型, 我们不必为每个形参都写出参数类型。
func add(x, y int) int {
return x + y
}
函数的类型被称为函数的标识符。 如果两个函数符合下面的条件,则表示这2个函数有相同的类型和标识符:
1. 形式参数列表一一对应
2. 返回值列表中的变量类型一一对应
注:在函数调用时, Go语言没有默认参数值。
在函数体中,实参通过值的方式传递, 因此函数的形参是实参的拷贝。 对形参进行修改不会影响实参。但是如果实参包括引用类型, 如指针, slice(切片)、 map、 function、 channel等类型, 实参可能会由于函数的间接引用被修改。
递归
这里有个实例: https://gitee.com/zhexiao/codes/cxj7u6wf4osiag0v2dqr824
1. main函数解析HTML输入,通过递归函数visit获得links(链接),并打印出这些links
2. visit函数遍历HTML的节点树, 从每一个anchor元素的href属性获得link,将这些links存入字符串数组中。为了遍历结点n的所有后代结点, 每次遇到n的孩子结点时, visit递归的调用自身。
部分代码如下:
func visit(links []string, n *html.Node) []string {
//visit函数遍历节点树得到link
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
//为了遍历结点n的所有后代结点
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
return links
}
大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度, 当你用递归处理大量数据时, 需要避免栈溢出。
Go语言使用可变栈, 栈的大小按需增加(初始时很小)。 这使得我们使用递归时不必考虑溢出和安全问题。
多返回值
按照惯例, 函数的最后一个bool类型的返回值表示函数是否运行成功, error类型的返回值代表函数的错误信息。
看示例:
func findLinks(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("get error %s", resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parse error %s", err)
}
return visit(nil, doc), nil
}
虽然Go的垃圾回收机制会回收不被使用的内存,但这不包括操作系统层面的资源,比如打开的文件、网络连接。因此我们必须显式的释放这些资源。针对上面就是:resp.Body.Close()
调用多返回值函数时, 返回给调用者的是一组值, 调用者必须显式的将这些值分配给变量,如果某个值不被使用, 可以将其分配给blank identifier。
links, err := findLinks(url)
或者
links, _ := findLinks(url)
bare return
如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数,这称之为bare return。
按照返回值列表的次序,return语句等价于 return x, y
func abc() (x int, y string) {
x = 10
y = "zhexiao"
return
}
错误
如果导致失败的原因只有一个, 额外的返回值可以是一个布尔值,通常被命名为ok。如果导致失败的原因不止一种,则不能使用简单的bool类型,而应该使用error类型。
比如, cache.Lookup失败的唯一原因是key不存在:
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}
fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回:
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("get error %s", resp.Status)
}
如果出现错误我们需要结束程序,这种策略一般只应在main中执行,对库函数而言,应仅向上传播错误:
func main() {
url := "http://sina.com.cn"
links, err := findLinks(url)
if err != nil{
fmt.Fprintf(os.Stderr, "err: %v\n", err)
os.Exit(1)
}
}
可以调用log.Fatalf()达到与上面一样的效果。log中的所有函数,都默认会在错误信息之前输出时间信息。:
func main() {
url := "http://sina.com.cn"
links, err := findLinks(url)
if err != nil{
//2018/07/02 14:15:47 err: Get sina.com.cn: unsupported protocol scheme
log.Fatalf("err: %v\n", err)
}
}
如果出现错误我们需要输出错误信息,不需要中断程序的运行:
log.Printf("ping failed: %v; networking disabled",err)
或者
fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
EOF(文件结尾错误)
io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF。
r, _, err := in.ReadRune()
if err == io.EOF {
break // finished reading
}
函数值
在Go中, 函数被看作第一类值( first-class values) : 函数值像其他值一样, 拥有类型, 可以被赋值给其他变量, 传递给函数, 从函数返回。
下面的例子中,f赋值给product的时候出错,记得上面我们说过,函数的类型一样必须是参数和返回值都一一对应,所以f不能赋予给product。
func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
func main() {
f := square
fmt.Println(f(3))
f = negative
fmt.Println(f(3))
fmt.Printf("%T\n", f)
//error,类型不一样
f = product
}
函数类型的零值是nil
var f func(int) int
函数值可以与nil比较,但是函数值之间是不可比较的, 也不能用函数值作为map的key。
var f func(int) int
if f != nil {
f(3)
}
函数作为参数传入:
func square(n int) int {
return n * n
}
func getData(n int, s func(n int) int) int {
data := s(n)
return data
}
func main() {
data := getData(10, square)
fmt.Println(data)
}
匿名函数
函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式, 它的值被成为匿名函数。
例如下面读取unicode字符:
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着函数的变量可以被该函数中定义的匿名函数使用, 如下例所示:
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
}
注:在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数在squares中存在变量引用。
拓扑排序
下面的代码用深度优先搜索了整张图, 获得了符合要求的课程序列。完整代码: https://gitee.com/zhexiao/codes/7mn0834gs5bix1fw9rqch15
部分代码如下:
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items{
if !seen[item]{
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
当匿名函数需要被递归调用时, 我们必须首先声明一个变量,再将匿名函数赋值给这个变量。如果不分成两部, 函数字面量无法与visitAll绑定, 我们也无法递归调用该匿名函数。
visitAll := func(items []string) {
// ...
visitAll(m[item]) // compile error: undefined: visitAll
// ...
}
但是当匿名函数不需要被递归调用,当然可以直接赋予函数值了。
捕获迭代变量
考虑这个样一个问题: 你被要求首先创建一些目录, 再将目录删除。
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
}
你可能会感到困惑, 为什么要在循环体中用循环变量d赋值一个新的局部变量, 而不是像下面的代码一样直接使用循环变量dir。 需要注意, 下面的代码是错误的。
var rmdirs []func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) // NOTE: incorrect!
})
}
原因:这个问题的原因在于循环变量的作用域,上面的循环变量dir在for循环这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量,又因为函数值中记录的是dir的内存地址而不是某一时刻的值。所以当最后操作删除执行的时候,dir存储的值是最后一次循环的值,等于删除的都是同一个目录。
通常, 为了解决这个问题, 我们会引入一个与循环变量同名的局部变量, 作为循环变量的副本。
for _, dir := range tempDirs() {
dir := dir // declares inner dir, initialized to outer dir
// ...
}
对循环变量i的使用也存在同样的问题:
func main() {
var numList = []int{1, 2, 3, 4, 5}
var sqFuncs []func()
for _, i := range numList {
sqFuncs = append(sqFuncs, func() {
fmt.Println(i * i)
})
}
for _, sqFunc := range sqFuncs{
sqFunc()
}
}
上面的打印全部都是25。正确的方式应该是新增局部变量:
for _, i := range numList {
i := i
.....
如果你使用go语句或者defer语句会经常遇到此类问题。 这不是go或defer本身导致的, 而是因为它们都会等待循环结束后, 再执行函数值。
可变参数
参数数量可变的函数称为为可变参数函数。在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”。这表示该函数会接收任意数量的该类型参数。
例如:sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[]int的切片。
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
fmt.Println(sum(1, 2, 3, 4))
上面的代码中,其实是隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。
如果原始参数已经是切片类型, 我们该如何传递给sum?只需在最后一个参数后加上省略符。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...))
Deferred函数
defer语句经常被用于处理成对的操作, 如打开、 关闭、 连接、 断开连接、 加锁、 释放锁。
在调用普通函数或方法前面加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时, 跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行。你可以在一个函数中执行多条defer语句, 它们的执行顺序与声明顺序相反。
例如:io资源
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ReadAll(f)
}
例如:处理互斥锁
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
调试复杂程序时, defer机制也常被用于记录何时进入和退出函数。
注: 因为trace返回了一个匿名函数,不要忘记defer语句后的圆括号。否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的, 永远不会被执行。
func bigSlowOperation() {
//注意这里后面还有个括号
defer trace("bigSlowOperation")()
time.Sleep(2 * time.Second)
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg, time.Since(start))
}
}
func main() {
bigSlowOperation()
}
被延迟执行的匿名函数修改函数返回值:
func triple(x int) (result int) {
defer func() {
result += x
}()
return x * 2
}
func main() {
fmt.Println(triple(5))
}
在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽:
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file
// ...process f…
}
Panic异常
Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。 这些运行时错误会引起painc异常。
一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer)。
直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。
比如,当程序到达了某条逻辑上不可能到达的路径:
func Reset(x *Buffer) {
if x == nil {
panic("x is nil") // unnecessary!
}
}
虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误。
为了方便诊断问题,runtime包允许程序员输出堆栈信息:
func f(x int){
x = x * 100
panic("error happens")
}
func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}
func main() {
defer printStack()
f(3)
}
你可能会惊讶runtime.Stack为什么能够输出已经被释放的函数信息,这是因为在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。
Recover异常捕获
通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。
如果在deferred函数中调用了内置函数recover,并且在定义该defer语句的函数中发生了panic异常,recover会使程序从panic中恢复,并返回panic value。
例如:deferred函数帮助Parse从panic中恢复,在deferred函数内部,panic value被附加到错误信息
中;并用err变量接收错误信息,返回给调用者。
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}