一個函數(shù)如果有命名的返回值,可以省略 return 語句的操作數(shù),這稱為裸返回。
在一個函數(shù)中如果存在許多返回語句且有多個返回結(jié)果,裸返回可以消除重復(fù)代碼,但是并不能使代碼更加易于理解。比如,對于這種方式,在第一眼看來,不能直觀地看出返回的值具體是什么。如果之前一直沒有使用過返回值的變量名,返回變量的零值,如果賦過值了,則返回新的值,這就有可能會看漏。鑒于這個原因,應(yīng)該保守使用裸返回。
站在用戶的角度思考問題,與客戶深入溝通,找到新巴爾虎左網(wǎng)站設(shè)計與新巴爾虎左網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗,讓設(shè)計與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:網(wǎng)站制作、成都網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、域名申請、虛擬空間、企業(yè)郵箱。業(yè)務(wù)覆蓋新巴爾虎左地區(qū)。
在下面的例子中,變量 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語句造成的。
下面的用法是錯誤的:
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)前值。
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)用的。
Go 語言的類型系統(tǒng)會在編譯時捕獲很多錯誤,但有些錯誤只能在運行時檢查,如數(shù)組訪問越界、空指針引用等。這些運行時錯誤會引起painc異常。
可以直接調(diào)用內(nèi)置的 panic 函數(shù)。如果碰到“不可能發(fā)生”的狀況,panic 是最好的處理方式,比如語句執(zhí)行到邏輯上不可能到達(dá)的地方時。
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)用。
退出程序通常是正常的處理panic異常的方式。但有時需要從異常中恢復(fù),至少可以在程序崩潰前做一些操作。
將內(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...
}
對于 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)程。
使用 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")
}