golang 函数

函数声明

函数声明包括函数名、 形式参数列表、返回值列表(可省略)以及函数体。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

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

猜你喜欢

转载自blog.csdn.net/andybegin/article/details/80942482