溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

gopl 函數(shù)

發(fā)布時間:2020-07-27 20:53:45 來源:網(wǎng)絡(luò) 閱讀:518 作者:騎士救兵 欄目:編程語言

裸返回

一個函數(shù)如果有命名的返回值,可以省略 return 語句的操作數(shù),這稱為裸返回。
在一個函數(shù)中如果存在許多返回語句且有多個返回結(jié)果,裸返回可以消除重復(fù)代碼,但是并不能使代碼更加易于理解。比如,對于這種方式,在第一眼看來,不能直觀地看出返回的值具體是什么。如果之前一直沒有使用過返回值的變量名,返回變量的零值,如果賦過值了,則返回新的值,這就有可能會看漏。鑒于這個原因,應(yīng)該保守使用裸返回。

圖的遍歷

在下面的例子中,變量 prereqs 的 map 提供了很多課程(key),以及學(xué)習(xí)該課程的前置條件(value):

var prereqs = map[string][]string{
    "algorithems": {"data structures"},
    "calculus":    {"linear algebra"},
    "compilers": {
        "data structures",
        "formal languages",
        "computer organization",
    },
    "data structures":       {"discrete math"},
    "databases":             {"data structures"},
    "discrete math":         {"intro to programming"},
    "formal languages":      {"discrete math"},
    "networks":              {"operating systems"},
    "operating systems":     {"data structures", "computer organization"},
    "programming languages": {"data structures", "computer organization"},
}


這樣的問題是一種拓?fù)渑判?。概念上,先決條件的內(nèi)容構(gòu)成了一張有向圖,每一個節(jié)點代表一門課程。每一條邊代表一門課程所依賴的另一門課程的關(guān)系。
圖是無環(huán)的:沒有節(jié)點可以通過圖上的路徑回到它自己。

可以使用深度優(yōu)先的搜索計算得到合法的學(xué)習(xí)路徑,代碼入下所示:

func main() {
    for i, course := range topoSort(prereqs) {
        fmt.Printf("%d:\t%s\n", i+1, course)
    }
}

func topoSort(m map[string][]string) []string {
    // 閉包的部分
    var order []string
    seen := make(map[string]bool)
    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)
            }
        }
    }
    // 主體
    var keys []string
    for key := range m {
        keys = append(keys, key)
    }
    sort.Strings(keys)
    visitAll(keys)
    return order
}

當(dāng)一個匿名函數(shù)需要進(jìn)行遞歸,必須先聲明一個變量然后將匿名函數(shù)賦給這個變量。如果將兩個步驟合并成一個聲明,函數(shù)字面量將不會存在于該匿名函數(shù)的作用域中,這樣就不能遞歸地調(diào)用自己了。
下面是拓?fù)渑判虻某绦蜉敵觯谴_定的結(jié)果,就是每次執(zhí)行都一樣。這里輸出時調(diào)用的是切片而不是 map,所以迭代的順序是確定的并且在調(diào)用最初的 map 之前是對它的 key 進(jìn)行了排序的。

PS H:\Go\src\gopl\ch6\toposort> go run main.go
1:      intro to programming
2:      discrete math
3:      data structures
4:      algorithems
5:      linear algebra
6:      calculus
7:      formal languages
8:      computer organization
9:      compilers
10:     databases
11:     operating systems
12:     networks
13:     programming languages
PS H:\Go\src\gopl\ch6\toposort>

警告:捕獲迭代變量

首先,看下面的代碼:

package main

import "fmt"

func main() {
    var shows []func()
    for _, v := range []int{1, 2, 3, 4, 5} {
        shows = append(shows, func() { fmt.Println(v) })
    }

    for _, f := range shows {
        f()
    }
}

這里的期望是依次打印每個數(shù)。但實際打印出來的全部都是5。
在for循環(huán)引進(jìn)的一個塊作用域內(nèi)聲明了變量v,然后到了循環(huán)里使用的這類變量共享相同的變量,即一個可訪問的存儲位置,而不是固定的值。v的值在不斷地迭代中更新,因此當(dāng)之后調(diào)用打印的時候,v變量已經(jīng)被每一次的for循環(huán)更新多次。所以打印出來的是最后一次迭代時的值。
這里可以通過引入一個內(nèi)部變量來解決這個問題,可以換個名字,也可以使用一樣的變量名:

func main() {
    var shows []func()
    for _, v := range []int{1, 2, 3, 4, 5} {
        v := v // 這句是關(guān)鍵
        shows = append(shows, func() { fmt.Println(v) })
    }

    for _, f := range shows {
        f()
    }
}

看起來奇怪,但卻是一個關(guān)鍵性的聲明。for循環(huán)內(nèi)也可以隨意定義一個不一樣的變量名,這樣看著更好理解一些。
也可以用匿名函數(shù)(閉包)來理解,這里確實是一個閉包,匿名函數(shù)內(nèi)引用了外部變量。第一個示例中,變量v會在for循環(huán)的每次迭戈中更新。第二個示例,匿名函數(shù)引用的變量v是在for循環(huán)內(nèi)部聲明的,不會隨著迭代而更新,并且在for循環(huán)內(nèi)部也沒有變化過。
這樣的隱患不僅僅存在于使用range的for循環(huán)里。在 for i := 0; i < 10; i++ {} 這樣的循環(huán)里作用域也是同樣的,這里的變量i也是會有同樣的問題,需要避免。
另外在go語句和derfer語句的使用當(dāng)中,迭代變量捕獲的問題是最頻繁的,這是因為這兩個邏輯都會推遲函數(shù)的執(zhí)行時機(jī),直到循環(huán)結(jié)束。但是這個問題并不是有g(shù)o或者defer語句造成的。

goroutine 中同樣的問題

下面的用法是錯誤的:

for _, f := range names {
    go func() {
        call(f) // 注意:不正確
    }
}

需要作為一個字面量函數(shù)的顯式參數(shù)傳遞 f,而不是在 for 循環(huán)中聲明 f。正確的做法如下:

for _, f := range names {
    go func(f string) {
        call(f)
    }(f) // 顯式的傳遞 f 給函數(shù)
}

像上面這樣,通過添加顯式參數(shù),可以確保當(dāng) go 語句執(zhí)行的時候,使用 f 的當(dāng)前值。

延遲函數(shù)調(diào)用(defer)

defer 語句也可以用來調(diào)試一個復(fù)雜的函數(shù),即在函數(shù)的“入口”和“出口”處設(shè)置調(diào)試行為。下面的 bigSlowOperation 函數(shù)在開頭調(diào)用 trace 函數(shù),在函數(shù)剛進(jìn)入的時候執(zhí)行輸出,然后返回一個函數(shù)變量,當(dāng)其被調(diào)用的時候執(zhí)行退出函數(shù)的操作。以這種方式推遲返回函數(shù)的調(diào)用,就可以使一個語句在函數(shù)入口和所有出口添加處理,甚至可以傳遞一些有用的值,比如每個操作的開始時間:

package main

import (
    "log"
    "time"
)

func bigSlowOperation() {
    defer trace("bigSlowOperation")()  // 這個小括號很重要
    // ...這里假設(shè)有一些操作...
    time.Sleep(3 * 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()
}

通常的defer語句提供一個函數(shù),會在函數(shù)退出時再調(diào)用。
上面的defer語句,最后面有兩個小括號。trace函數(shù)調(diào)用后會返回一個匿名函數(shù),加上后面的小括號才是延遲調(diào)用執(zhí)行的部分。而trace函數(shù)本身則會在當(dāng)前位置就執(zhí)行,并且返回匿名函數(shù)給defer語句。在trace函數(shù)獲取返回值的過程中,也就是trace函數(shù)里,會先執(zhí)行兩行語句,獲取start變量的值以及輸出一行信息,這個是在函數(shù)開頭就執(zhí)行的。最后函數(shù)返回的匿名函數(shù)是提供給defer語句在退出的時候進(jìn)行延遲調(diào)用的。

Panic異常

Go 語言的類型系統(tǒng)會在編譯時捕獲很多錯誤,但有些錯誤只能在運行時檢查,如數(shù)組訪問越界、空指針引用等。這些運行時錯誤會引起painc異常。

主動調(diào)用 panic

可以直接調(diào)用內(nèi)置的 panic 函數(shù)。如果碰到“不可能發(fā)生”的狀況,panic 是最好的處理方式,比如語句執(zhí)行到邏輯上不可能到達(dá)的地方時。

轉(zhuǎn)儲棧信息

runtime 包提供了轉(zhuǎn)儲棧的方法是程序員可以診斷錯誤,下面的代碼在 main 函數(shù)中延遲 printStack 的執(zhí)行:

package main

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

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.WriteString("Stack 中的內(nèi)容:\n")
    os.Stdout.Write(buf[:n])
    os.Stdout.WriteString("Stack 結(jié)束...\n")
}

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

Panic之后,在退出前會調(diào)用 defer 的內(nèi)容,輸出 buf 中的棧信息。最后還會輸出宕機(jī)消息到標(biāo)準(zhǔn)輸出流。
runtime.Stack 能夠輸出函數(shù)棧信息,在其他語言中,此時函數(shù)棧的信息應(yīng)該已經(jīng)不存在了。但是 Go 語言的宕機(jī)機(jī)制讓延遲執(zhí)行的函數(shù)在棧清理之前調(diào)用。

Recover捕獲異常

退出程序通常是正常的處理panic異常的方式。但有時需要從異常中恢復(fù),至少可以在程序崩潰前做一些操作。

recover函數(shù)

將內(nèi)置的 recover 函數(shù)在延遲函數(shù)的內(nèi)部調(diào)用,當(dāng)定義了該 defer 語句的函數(shù)發(fā)生了 panic 異常,recover 就會終止當(dāng)前的 panic 狀態(tài)并且返回 panic value。函數(shù)不會從之前 panic 的地方繼續(xù)運行而是正常返回。在未發(fā)生 panic 時調(diào)用 recover 則沒有任何效果并且返回 nil。

舉例說明

假設(shè)有一個語言解析器。即使看起來運行正常,但考慮到工作的復(fù)雜性,還是會存在只在特殊情況下發(fā)生的 bug。此時我們更希望返回一個錯誤 error 而不是導(dǎo)致程序崩潰 panic。所以 panic 發(fā)生后,不要立即終止運行,而是將一些有用的附加消息提供給用戶來報告這個bug。下面是使用 recover 部分的代碼:

func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
    // ...parser...
}

恢復(fù)的原則

對于 panic 采用無差別的恢復(fù)措施是不可靠的。
從同一個包內(nèi)發(fā)生的 panic 進(jìn)行恢復(fù)有助于簡化處理復(fù)雜和未知的錯誤,但一般的原則是,不應(yīng)該嘗試去恢復(fù)從另一個包內(nèi)發(fā)生的 panic。公共的 API 應(yīng)該直接報告錯誤。同樣,也不應(yīng)該恢復(fù)一個 panic,而這段代碼卻不是由你來維護(hù)的,比如調(diào)用這提供的回調(diào)函數(shù),因為你不清楚這樣做是否安全。
有時也很難完全遵循規(guī)范,舉個例子,net\/http包中提供了一個web服務(wù)器,將收到的請求分發(fā)給用戶提供的處理函數(shù)。很顯然,我們不能因為某個處理函數(shù)引發(fā)的panic異常,影響整個進(jìn)程導(dǎo)致退出。web服務(wù)器遇到處理函數(shù)導(dǎo)致的panic時會調(diào)用recover,輸出堆棧信息,繼續(xù)運行。這樣的做法在實踐中很便捷,但也會有一定的風(fēng)險,比如導(dǎo)致資源泄漏或是因為recover操作,導(dǎo)致其他問題。
所以,最安全的做法就是選擇性地使用 recover。當(dāng) panic 之后需要進(jìn)行恢復(fù)的情況本來就不多。為了標(biāo)識某個 panic 是否應(yīng)該被恢復(fù),我們可以將 panic value 設(shè)置成特殊類型。在 recover 時對 panic value 進(jìn)行檢查,如果發(fā)現(xiàn) panic value 是特殊類型,就將這個 panic 作為 errror 處理。如果不是,則按照正常的 panic 進(jìn)行處理。
下面示例代碼中的 soleTitle 函數(shù)就是一個這樣的例子:

package main

import (
    "fmt"
    "net/http"
    "os"
    "strings"

    "golang.org/x/net/html"
)

func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
    if pre != nil {
        pre(n)
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        forEachNode(c, pre, post)
    }
    if post != nil {
        post(n)
    }
}

// soleTitle 返回文檔中一個非空標(biāo)題元素
// 如果沒有標(biāo)題則返回錯誤
func soleTitle(doc *html.Node) (title string, err error) {
    type bailout struct{}

    defer func() {
        switch p := recover(); p {
        case nil:
            // 沒有宕機(jī)
        case bailout{}:
            // 預(yù)期的宕機(jī)
            err = fmt.Errorf("multiple title elements")
        default:
            panic(p) // 未預(yù)期的宕機(jī),繼續(xù)宕機(jī)過程
        }
    }()
    // 如果發(fā)現(xiàn)多余一個非空標(biāo)題,退出遞歸
    forEachNode(doc, func(n *html.Node) {
        if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil {
            if title != "" {
                panic(bailout{}) // 多個標(biāo)題元素
            }
            title = n.FirstChild.Data
        }
    }, nil)
    if title == "" {
        return "", fmt.Errorf("no title element")
    }
    return title, nil
}

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

    // 檢查返回的頁面是HTML通過判斷Content-Type,比如:Content-Type: text/html; charset=utf-8
    ct := resp.Header.Get("Content-Type")
    if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
        return fmt.Errorf("%s has type %s, not text/html", url, ct)
    }

    doc, err := html.Parse(resp.Body)
    if err != nil {
        return fmt.Errorf("parseing %s as HTML: %v", url, err)
    }

    title, err := soleTitle(doc)
    if err != nil {
        return err
    }
    fmt.Println(title)
    return nil
}

func main() {
    for _, arg := range os.Args[1:] {
        if err := title(arg); err != nil {
            fmt.Fprintf(os.Stderr, "title: %v\n", err)
        }
    }
}

defer 調(diào)用 recover,檢查 panic value,如果該值是 bailout{} 則返回一個普通的錯誤。所有其他非空的值都是預(yù)料外的 panic,這時繼續(xù)使用 panic value 的值作為參數(shù)調(diào)用 panic。

這個示例里,違反了 panic 不處理"預(yù)期"錯誤的建議,但是這里是為了展示這種處理 panic 的機(jī)制:

if title != "" {
    panic(bailout{}) // 多個標(biāo)題元素
}

對于一個預(yù)期的錯誤,比如這里標(biāo)題為空的情況。正常編寫程序的時候,不應(yīng)該調(diào)用panic,而是進(jìn)行處理,比如返回 error。

有些情況下是沒有恢復(fù)動作的。比如,內(nèi)存耗盡會使 Go 運行時發(fā)生嚴(yán)重錯誤而直接終止進(jìn)程。

練習(xí)

使用 panic 和 recover 寫一個函數(shù),它沒有 return 語句,但是能夠返回一個非零的值。

package main

import "fmt"

func main() {
    s := noRet()
    fmt.Println(s)
}

func noRet() (s string) {
    defer func() {
        p := recover()
        s = fmt.Sprint(p)
    }()
    panic("Hello")
}
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI