Golang 語法速查(二)
1. 併發 (Concurrency)
Goroutines
go say("world") // 啟動一個新的 goroutine
Channels
ch := make(chan int) // 創建 channel
// 發送
go func() { ch <- 42 }()
// 接收
v := <-ch
// Buffered Channel
ch2 := make(chan int, 100)
// 關閉 Channel
close(ch)
// Range Channel
for i := range ch {
fmt.Println(i)
}
Select
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
default:
fmt.Println("waiting")
}
2. 錯誤處理 (Error Handling)
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
3. Defer
Go 的 defer 語句用於預設一個函數調用(即推遲執行函數),該函數會在執行 defer 的函數返回之前立即執行。它顯得非比尋常,但卻是處理一些事情的有效方式,例如無論以何種路徑返回,都必須釋放資源的場景。典型的例子就是解鎖互斥鎖和關閉文件。
Go's defer statement schedules a function call (the deferred function) to be run immediately before the function executing the defer returns. The canonical examples are unlocking a mutex or closing a file.
基本範例
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// 輸出:
// hello
// world
典型用法:關閉文件
// Contents 將文件內容作為字串返回
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close 會在函數結束時執行,無論從哪個 return 離開
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...)
if err != nil {
if err == io.EOF {
break
}
return "", err // 在這裡 return 時,f 也會被關閉
}
}
return string(result), nil // 在這裡 return 時,f 也會被關閉
}
推遲 Close 的兩點好處:
- 不會忘記關閉:若日後為函數新增其他 return 路徑,仍能確保文件被關閉。
- 開與關相鄰:
Close緊跟在Open之後,比放在函數結尾更清晰。
參數求值時機
被推遲函數的實參(若為方法則包含接收者)是在 defer 語句執行時就求值,而不是在「真正調用執行時」才求值。因此不用擔心執行過程中變數被改動,也意味著同一個 defer 可以推遲多個函數執行。
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
// 函數返回時輸出: 4 3 2 1 0
被推遲的函數按 LIFO(後進先出) 順序執行,所以上面會印出 4 3 2 1 0。
為什麼 Go 要這樣設計? 這不只是為了好玩,defer 最常見的用途是清理資源(例如關閉檔案或資料庫連線)。
想像你寫了一個函式,流程如下:
打開檔案 A
打開檔案 B
處理資料...
如果你在打開 A 之後寫一個 defer A.Close(),打開 B 之後寫一個 defer B.Close()。 根據後進先出的原則,程式會先關閉 B,再關閉 A。這非常符合邏輯,因為 B 可能依賴 A 才能存在,先拆掉後蓋好的東西,通常比較安全且不會出錯。
追蹤函數執行
可用一對簡單的 trace / untrace 來追蹤函數進入與離開:
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
func a() {
trace("a")
defer untrace("a")
// 做一些事情....
}
利用「defer 的實參在 defer 執行時就求值」的特性,可以讓 untrace 的參數由 trace 來設定:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
// 輸出:
// entering: b
// in b
// entering: a
// in a
// leaving: a
// leaving: b
對於習慣其他語言「塊級」資源管理的程式員,defer 可能顯得特別,但其有趣且強大的應用,恰恰來自於它是基於函數而非基於塊。在 panic 與 recover 章節還會看到更多應用。