《Go程序设计语言》要点总结——函数


1. 基本知识点

  1. 函数声明:

    func 函数名(参数列表) (返回值列表) {
        函数体
    }
    
    /* 例子 */
    func exp1(x, y int) int {   //如果返回值只有一个,那可以省略括号
        return x + y
    }
    
    func exp2(x, y int) {   // 如果没有返回值,则可以全部省略。
        x + y
    }
  2. 函数的参数列表、返回值列表构成函数的签名(只与参数类型及个数有关,与具体名字无关)。

  3. 函数参数默认是传值的(passed by value),但当参数是pointer、slice、map、function、channel时,是传引用的。

  4. 如果一个函数声明没有函数体部分,则说明它是非Go语言实现的。

    package math
    
    func Sin(x float64) float64 // 使用汇编语言实现
  5. 在很多语言中,我们使用递归的时候需要考虑栈溢出的问题,因为栈大小往往是固定的(典型的取值范围是64KB~2MB)。但是在Go中,我们基本不需要考虑这个问题。Go实现的函数调用栈是变长的,最大可以到GB级别。

  6. Go函数可以有多个返回值。这里有一个小知识点就是空返回(bare return)。比如我们的函数的返回值比较多的时候,如果函数体内返回的分支比较多,那每个地方都return那么多变量,写起来很麻烦。所以Go提供了bare return。所谓bare return就是在定义函数的时候,返回值列表里面就把每个返回值的名字写上。这样,在每个需要返回的地方直接写个return就行了,各个变量会自动返回,值就是当前状态下变量的值。看个例子:

    func CountWordsAndIages(url string) (words, images int, err error) {
        resp, err := http.Get(url)
        if err != nil {
            return
        }
    
        doc, err := html.Parse(resp.body)
        resp.Body.Close()
        if err != nil {
            err = fmt.Errorf("Parsing HTML: %s", err)
            return
        }
        words, images = countWordsAndImages(doc)
        return

    每个return的地方其实相当于return words, images, err。不过这种写法会使代码可读性变差,慎重使用。

  7. I/O时因为读到文件尾而导致的失败均返回io.EOF.

2. Function Value

我不知道该如何翻译这个,我看中文版翻译的是“作为值得函数”,感觉怪怪的,我还是沿用英文叫法吧。

Functions are first-class values in Go:like other values, function values have types, and they may be assigned to variables or passed to or returned from functions. A function value may be called like any other function.

先看个例子:

func square(n int) int    { return n*n }
func negative(n int) int    { -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  // compile error:can't assign f(int, int) int to f(int) int

通过上面这个例子可以看出来,某种程度上Function value有点像C里面的函数指针(但实际不一样)。函数类型的变量的默认值是nil,调用值为nil的Function value会导致panic。

var f func(int) int
f(3)    // panic: call of nil function

Function value是不可比较类型,但它可以和nil比较(之前已经见过很多不可比较的类型都支持和nil比较),所以使用Function value之前可以先判是否为nil:

var f func(int) int
if f != nil {
    f(3)
}

其实Function value还有一个重要的特性就是它是有状态的,在下一节的例子中我们会看到。

为了更好的理解Function value,我们简单介绍一下所谓的“first-class value”。这个其实是个通用概念(以下内容摘录自维基百科:《第一类对象》):

第一类对象(英语:First-class object)在计算机科学中指可以在执行期创造并作为参数传递给其他函数或存入一个变数的实体[1]。将一个实体变为第一类对象的过程叫做“物件化”(Reification)。
In programming language design, a first-class citizen (also object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as a parameter, returned from a function, and assigned to a variable.

第一类对象不一定是面向对象程序设计所指的物件,而可以指任何程序中的实体。一般第一类对象所特有的特性为:

  • 可以被存入变量或其他结构

  • 可以被作为参数传递给其他函数

  • 可以被作为函数的返回值

  • 可以在执行期创造,而无需完全在设计期全部写出

  • 即使没有被系结至某一名称,也可以存在(指对象有内部表示,而不是根据名字来识别,比如匿名函数,还可以通过赋值叫任何名字)

绝大多数语言中,数值与基础型别都是第一类对象,然而不同语言中对函数的区别很大,例如C语言与C++中的函数不是第一类对象,因为在这些语言中函数不能在执行期创造,而必须在设计时全部写好。相比之下,Scheme中的函数是第一类对象,因为可以用lambda语句来创造匿名函数并作为第一类对象来操作。

3. 匿名函数

所谓匿名函数,简单理解就是没有名字的函数。如上面第一类对象的最后一条特性一样。匿名函数可以给我们提供很多便利。普通的函数只能在包中事先定义好,但匿名函数却像表达式一样,在任何我们需要的地方定义。它的定义方法就是在普通函数定义中去掉函数名即可。看一个例子:

package main

import (
    "fmt"
)

func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}

func main() {
    f := squares()
    fmt.Printf("%T\n", f)
    fmt.Println(f())    // "1"
    fmt.Println(f())    // "4"
    fmt.Println(f())    // "9"
    fmt.Println(f())    // "16"
}

这个例子中,square函数创建了一个局部变量x,并返回一个匿名函数,类型是func() int。每次调用这个匿名函数都会返回x的平方。这里有几个特别重要的知识点:

  1. 像上面的square一样在函数体内定义的匿名函数是可以访问到它外部包裹它的函数的所有变量的(inner function can refer to variables from the enclosing function)。

  2. main函数中,当square返回后,它定义的局部变量x依旧是存在的。这个之前就介绍过,在Go中,变量的生命周期(lifetime)不是由它的作用域(scope)决定的,而是得看有没有人具体使用它。因为Go不像C,它有自己的GC,可以保证这边变量离开它的作用域后,如果没有人使用,依旧可以被释放而不造成内存泄漏。

  3. 我们多次调用f,x的值会递增。这就是上一节最后说到的Function value是有状态的,它能记住之前的状态。

  4. 匿名函数因为没有名字,所以它的递归有些特别:我们必须先声明一个变量,然后再把匿名函数赋给这个变量才可以。下面分别看个错误与正确的例子:

    /* 错误的例子 */
    visitAll := func(items []string) {
        // ...
        visitAll(m[item])    // compile error: undefined: visitAll
        // ...
    }
    
    /* 正确的例子 */
    // ...
    var VisitAll func(items []string)
    visitAll = func(items []string) {
        // ...
        visitAll(m[item])
        // ...
    }
    // ...

4. 变参函数

Go也提供了变参函数,定义方法就是在最后一个参数的类型前面加上英文省略号(ellipsis:...):

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

fmt.Println(sum())                 // "0"
fmt.Println(sum(3))                // "3"
fmt.Println(sum(1, 2, 3, 4))       // "10"

底层的实现是:调用者会分配一个数组来存放所有的参数,然后用这个数组构造一个slice传给函数。所以,如果参数已经是一个slice的话,可以直接传给变参函数,方法是给最有一个参数后面加上英文省略号

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

尽管如此,变长函数与参数是slice的函数仍然是不同的。

5. defer表达式

defer表达式又称延迟函数调用(Deferred Function Calls):

A defer statement is an ordinary function or method call prefixed by the keyword defer. The function and argument expressions are evaluated when the statement is executed, but the actual call is deferred until the function that contains the defer statement has finished, whether normally, by executing a return statement or failing off the end, or abnormally, by panicking. Any number of calls may be deferred; they are executed in the reverse of the order in which they were deferred.

其实简单理解Go中的defer类似于C语言中的atexit,不过更加轻量级了,但是功能基本是一样的。都是先注册一些函数,然后等主函数退出的时候就按照反序调用,一般都是为了释放资源。Go中推荐的做法是在刚成功申请到资源后就紧跟着写defer语句来释放资源。

看一些例子:

func title(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    // ...
}

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]
}

6. Panic和Recover

当Panic提供了程序异常时退出程序的一种方法,有点类似于C中的abort函数。使用panic需要注意以下几点:

  1. 调用panic会使程序crash,所以对于那些可以预期的错误,我们应该做正确的错误处理,而不是调用panic。

  2. 调用panic后,会执行defer语句,只有这些语句执行完了才会释放栈。所以我们可以在defer函数中安全的调用printStack之类的函数来捕获异常情况下的栈信息。

Recover就是为了Panic而生的。如果我们在panic之后调用的defer语句中使用了recover函数,它就会终止程序panic,使其回复到正常状态,并返回panic的值。如果在其他场景下调用recover函数,将没有任何作用,并且返回nil。Recover只是给我们提供了一个终止程序panic的机制,实际场景中,因为panic后,程序的状态是未知的(比如变量的值未知,数据结构是否完整未知,文件句柄、网络连接锁等是否释放也未知等等),所以使用中要特别谨慎,尽量在只有明确需要recover的地方才recover。


添加新评论

选择表情 captcha

友情提醒:不填或错填验证码会引起页面刷新,导致已填的评论内容丢失。

|