您好,登錄后才能下訂單哦!
一個函數如果有命名的返回值,可以省略 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語句造成的。
下面的用法是錯誤的:
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 語句也可以用來調試一個復雜的函數,即在函數的“入口”和“出口”處設置調試行為。下面的 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語句在退出的時候進行延遲調用的。
Go 語言的類型系統會在編譯時捕獲很多錯誤,但有些錯誤只能在運行時檢查,如數組訪問越界、空指針引用等。這些運行時錯誤會引起painc異常。
可以直接調用內置的 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 語言的宕機機制讓延遲執行的函數在棧清理之前調用。
退出程序通常是正常的處理panic異常的方式。但有時需要從異常中恢復,至少可以在程序崩潰前做一些操作。
將內置的 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")
}
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。