中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

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

gopl 函數

發布時間:2020-07-27 20:53:45 來源:網絡 閱讀:522 作者:騎士救兵 欄目:編程語言

裸返回

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

圖的遍歷

在下面的例子中,變量 prereqs 的 map 提供了很多課程(key),以及學習該課程的前置條件(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"},
}


這樣的問題是一種拓撲排序。概念上,先決條件的內容構成了一張有向圖,每一個節點代表一門課程。每一條邊代表一門課程所依賴的另一門課程的關系。
圖是無環的:沒有節點可以通過圖上的路徑回到它自己。

可以使用深度優先的搜索計算得到合法的學習路徑,代碼入下所示:

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
}

當一個匿名函數需要進行遞歸,必須先聲明一個變量然后將匿名函數賦給這個變量。如果將兩個步驟合并成一個聲明,函數字面量將不會存在于該匿名函數的作用域中,這樣就不能遞歸地調用自己了。
下面是拓撲排序的程序輸出,它是確定的結果,就是每次執行都一樣。這里輸出時調用的是切片而不是 map,所以迭代的順序是確定的并且在調用最初的 map 之前是對它的 key 進行了排序的。

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()
    }
}

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

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

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

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

goroutine 中同樣的問題

下面的用法是錯誤的:

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

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

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

像上面這樣,通過添加顯式參數,可以確保當 go 語句執行的時候,使用 f 的當前值。

延遲函數調用(defer)

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

package main

import (
    "log"
    "time"
)

func bigSlowOperation() {
    defer trace("bigSlowOperation")()  // 這個小括號很重要
    // ...這里假設有一些操作...
    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語句提供一個函數,會在函數退出時再調用。
上面的defer語句,最后面有兩個小括號。trace函數調用后會返回一個匿名函數,加上后面的小括號才是延遲調用執行的部分。而trace函數本身則會在當前位置就執行,并且返回匿名函數給defer語句。在trace函數獲取返回值的過程中,也就是trace函數里,會先執行兩行語句,獲取start變量的值以及輸出一行信息,這個是在函數開頭就執行的。最后函數返回的匿名函數是提供給defer語句在退出的時候進行延遲調用的。

Panic異常

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

主動調用 panic

可以直接調用內置的 panic 函數。如果碰到“不可能發生”的狀況,panic 是最好的處理方式,比如語句執行到邏輯上不可能到達的地方時。

轉儲棧信息

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

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")
    os.Stdout.Write(buf[:n])
    os.Stdout.WriteString("Stack 結束...\n")
}

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

Panic之后,在退出前會調用 defer 的內容,輸出 buf 中的棧信息。最后還會輸出宕機消息到標準輸出流。
runtime.Stack 能夠輸出函數棧信息,在其他語言中,此時函數棧的信息應該已經不存在了。但是 Go 語言的宕機機制讓延遲執行的函數在棧清理之前調用。

Recover捕獲異常

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

recover函數

將內置的 recover 函數在延遲函數的內部調用,當定義了該 defer 語句的函數發生了 panic 異常,recover 就會終止當前的 panic 狀態并且返回 panic value。函數不會從之前 panic 的地方繼續運行而是正常返回。在未發生 panic 時調用 recover 則沒有任何效果并且返回 nil。

舉例說明

假設有一個語言解析器。即使看起來運行正常,但考慮到工作的復雜性,還是會存在只在特殊情況下發生的 bug。此時我們更希望返回一個錯誤 error 而不是導致程序崩潰 panic。所以 panic 發生后,不要立即終止運行,而是將一些有用的附加消息提供給用戶來報告這個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...
}

恢復的原則

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

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 返回文檔中一個非空標題元素
// 如果沒有標題則返回錯誤
func soleTitle(doc *html.Node) (title string, err error) {
    type bailout struct{}

    defer func() {
        switch p := recover(); p {
        case nil:
            // 沒有宕機
        case bailout{}:
            // 預期的宕機
            err = fmt.Errorf("multiple title elements")
        default:
            panic(p) // 未預期的宕機,繼續宕機過程
        }
    }()
    // 如果發現多余一個非空標題,退出遞歸
    forEachNode(doc, func(n *html.Node) {
        if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil {
            if title != "" {
                panic(bailout{}) // 多個標題元素
            }
            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 調用 recover,檢查 panic value,如果該值是 bailout{} 則返回一個普通的錯誤。所有其他非空的值都是預料外的 panic,這時繼續使用 panic value 的值作為參數調用 panic。

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

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

對于一個預期的錯誤,比如這里標題為空的情況。正常編寫程序的時候,不應該調用panic,而是進行處理,比如返回 error。

有些情況下是沒有恢復動作的。比如,內存耗盡會使 Go 運行時發生嚴重錯誤而直接終止進程。

練習

使用 panic 和 recover 寫一個函數,它沒有 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問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

精河县| 土默特右旗| 云南省| 仙游县| 铁力市| 开江县| 广灵县| 兴仁县| 鹿泉市| 望奎县| 鲁山县| 美姑县| 汉阴县| 拜城县| 兴城市| 旌德县| 青冈县| 海林市| 调兵山市| SHOW| 镇坪县| 龙陵县| 洪江市| 三门峡市| 漳州市| 敦化市| 三门县| 于都县| 栖霞市| 微博| 洞口县| 左权县| 叙永县| 五河县| 自贡市| 泗水县| 洛扎县| 靖宇县| 沙洋县| 凤山县| 潼关县|