用 gopher 的方式使用 panic
Go 執行時(即成功編譯後,作業系統啟動該該程序)發生的錯誤會以 panics 的形式反饋。panic 可以通過這兩種形式觸發 :
1.直接使用內建panic 函式:
package main func main(){ panic("foo") }
> go install github.com/mlowicki/lab && ./bin/lab panic: foo goroutine 1 [running]: panic(0x56d20, 0x820142000) /usr/local/go/src/runtime/panic.go:481 +0x3e6 main.main() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:4 +0x65
panic 接受任何實現了空介面(interface{})的引數型別,而所有型別都實現了空介面
2.由於程式問題,產生執行時 的panic :
package main import "fmt" func f() int { return 0 } func main() { fmt.Println(1 / f()) }
> go install github.com/mlowicki/lab && ./bin/lab panic: runtime error: integer divide by zero [signal 0x8 code=0x7 addr=0x2062 pc=0x2062] goroutine 1 [running]: panic(0xda560, 0x8201c80b0) /usr/local/go/src/runtime/panic.go:481 +0x3e6 main.main() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:10 +0x22
觸發執行時的 panic 和通過介面型別ofollow,noindex" target="_blank">runtime.Error 的各種值呼叫 panic 函式在語義上是相同的。
第二種方式的輸出有一些關於訊號的額外資訊。*0x8 (SIGFPE)*報告了致命的算術錯誤。
為了更好了解 Go 的 panicking 機制,讓我們先深入瞭解一下 Go 語言程式中的幾種結構:
goroutines
用 Go 語言實現的程式在執行時或多或少都會有一些goroutine 。在關於go statement 的規範詳述中goroutine 定義如下:
一個 "Go" 語句在同一個地址空間內作為一個獨立的併發執行緒控制或者 goroutine ,開始執行函式呼叫。
Go 是一門併發語言,這是因為它(原生)提供了併發程式設計的特性,比如併發執行的語句 (Go 語句 )或能夠在一些併發事物中輕鬆交流的機制(channels )。 不過併發是什麼意思呢?和無處不在的並行又有什麼關係呢?
組成
併發是將一些東西作為一系列獨立的任務來構造的方式。它是一種結構(設計)。併發程式處理相互獨立的任務,也就是說,不關心它們的執行順序。並行是兩個或多個任務的同時執行。從字面上來理解,在同一時間很多執行執行緒正在進行中。它需要在多核機器上執行——沒有方式去「模仿」它 。
併發是比並行更為普遍的一個術語。併發程式的執行時間可以(但並不必要)更好利用多核,並且同時執行多個運算。如果只有一個核是可用的,那麼就要用到,比如時間切片(將時間分成一些分散的間隔,並且將它們分配給不同的任務),它仍然是併發的,但是並行在技術上是不可能的。
併發是同時處理很多事情,並行是同時進行很多事情。
Rob Pike
主 goroutine
從main 包(用 Go 寫的每一個程式的入口點)中執行main 函式。如果這個 Goroutine 在程式執行時結束,整個程式就終止了。執行時不會等待其他 Goroutine 結束。
程式從單個(主)goroutine 開始,在其生命週期中可以創造一些新的 goroutine(創造出有成千上萬個並不少見)。
goroutines 共用同一地址
package main import "fmt" func main() { i := 0 ch := make(chan int) Go func() { i++ ch <- i }() Go func() { i++ ch <- i }() fmt.Println(<-ch) fmt.Println(<-ch) }
> go install github.com/mlowicki/lab && ./bin/lab 1 2
Defer 語句
defer 可以讓函式在包含它的函式(使用 defer 的函式)結束後執行,方式如下:
1.在return 語句:
package main import "fmt" func f() int { fmt.Println("1") defer func() { fmt.Println("Inside deferred function") }() fmt.Println("2") return 1 } func main() { f() }
> go install github.com/mlowicki/lab && ./bin/lab 1 2 Inside deferred function
2.當執行到函式末尾:
package main import "fmt" func main() { fmt.Println("1") defer func() { fmt.Println("Inside deferred function") }() fmt.Println("2") }
> go install github.com/mlowicki/lab && ./bin/lab 1 2 Inside deferred function
3.在panicking 中:
package main import "fmt" func f() { fmt.Println("1") defer func() { fmt.Println("Inside deferred function") }() fmt.Println("2") panic("boom!") fmt.Println("3") } func main() { f() }
> go install github.com/mlowicki/lab && ./bin/lab 1 2 Inside deferred function panic: boom! goroutine 1 [running]: panic(0xb8260, 0x8202301f0) /usr/local/go/src/runtime/panic.go:481 +0x3e6 main.f() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:11 +0x20c main.main() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:16 +0x14
函式值和傳遞的引數在defer 語句處進行評定,而非在真正呼叫的時候發生:
package main import "fmt" func main() { f := func(n int) { fmt.Printf("Inside f, n=%d\n", n) } n := 1 defer f(n) f = func(int) { fmt.Println("Inside g") } n = 2 }
> go install github.com/mlowicki/lab && ./bin/lab Inside f, n=1
每個函式中可以有多個defer 語句。呼叫順序是後進先出的(就像 defer 的呼叫會被放入棧中):
package main import "fmt" func main() { defer func() { fmt.Println("1") }() defer func() { fmt.Println("2") }() defer func() { fmt.Println("3") }() }
> go install github.com/mlowicki/lab && ./bin/lab 3 2 1
這樣的方法呼叫也有效:
package main import "fmt" type T struct{} func (t T) m() { fmt.Println("Inside method") } func main() { t := T{} defer t.m() }
並且它會像你可能期待的那樣輸出“內部方法”
當函式值判定是 nil 時,程式會 panic。不過,當執行到defer 語句時不會發生,但是在實際呼叫被 defer 的函式時會發生 panic:
package main import "fmt" func main() { f := func() {} f = nil fmt.Println("Before defer statement") defer f() fmt.Println("After defer statement") }
> go install github.com/mlowicki/lab && ./bin/lab Before defer statement After defer statement panic: runtime error: invalid memory address or nil pointer dereference [signal 0xb code=0x1 addr=0x0 pc=0x549b3] goroutine 1 [running]: panic(0xda6c0, 0x8201c80e0) /usr/local/go/src/runtime/panic.go:481 +0x3e6 main.main() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:11 +0x1d5
內部 defer 的函式篡改已命名的返回引數是有可能的。如果沒有被命名,那麼通過閉包來更改返回變數值不會有任何影響:
import "fmt" func f() int { n := 0 defer func() { n++ }() return n } func g() (n int) { defer func() { n++ }() return n } func main() { fmt.Printf("f() == %d\n", f()) fmt.Printf("g() == %d\n", g()) }
> go install github.com/mlowicki/lab && ./bin/lab f() == 0 g() == 1
正如我們將在下面看到的那樣,defer 語句廣泛地應用於處理各種 panic(當然它們也可以應用於各種其他的方面,並不一定是處理各種 error)。
Panicking
當任意函式 f 發生 panic 時,我們在上面例子中已經看到,在 f 中呼叫延遲函式的函式將以後進先出的順序呼叫。之後將有什麼發生呢?之後對於 f 的呼叫者,這種過程將被重複——它的延遲的函式將被觸發。如此反覆直到 f 的 goroutine 中的最上面的那個函式。最後,最上面的那個函式的延遲的函式被呼叫,並且程式終止。就像是一個冒泡直到頂端的呼叫鏈:
package main import "fmt" func f(ch chan int) { defer func() { fmt.Println("Deferred by f") }() g() ch <- 0 } func g() { defer func() { fmt.Println("Deferred by g") }() h() } func h() { defer func() { fmt.Println("Deferred by h") }() panic("boom!") } func main() { ch := make(chan int) go f(ch) <-ch }
> go install github.com/mlowicki/lab && ./bin/lab Deferred by h Deferred by g Deferred by f panic: boom! goroutine 17 [running]: panic(0xb83e0, 0x820220050) /usr/local/go/src/runtime/panic.go:481 +0x3e6 main.h() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:24 +0x86 main.g() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:17 +0x35 main.f(0x820224000) /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:9 +0x35 created by main.main /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:29 +0x53
值得注意的是,無論 panic 在哪個 Goroutine 開始(主 Goroutine 或者是之後創造的),整個程式都會崩潰。
更多關於 Panicking
在延遲的函式內部觸發新的 panic 會怎樣呢?
import "fmt" func f(ch chan int) { defer func() { fmt.Println("Deferred by f") }() g() ch <- 0 } func g() { defer func() { fmt.Println("Deferred by g") }() h() } func h() { defer func() { fmt.Println("Deferred by h") }() defer func() { panic("2nd explosion!") }() panic("boom!") } func main() { ch := make(chan int) go f(ch) <-ch }
> go install github.com/mlowicki/lab && ./bin/lab Deferred by h Deferred by g Deferred by f panic: boom! panic: 2nd explosion! goroutine 5 [running]: panic(0xb8480, 0x8201c82d0) /usr/local/go/src/runtime/panic.go:481 +0x3e6 main.h.func2() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:25 +0x65 panic(0xb8480, 0x8201c82c0) /usr/local/go/src/runtime/panic.go:443 +0x4e9 main.h() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:27 +0xa3 main.g() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:17 +0x35 main.f(0x820214060) /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:9 +0x35 created by main.main /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:32 +0x53
不論如何,事實證明結果會是,在呼叫直到呼叫鏈的頂部的推遲的函式,這個過程將會執行。雖然會有,第二個 panic 這樣的新結果,像之前輸出的那樣,也會顯示出來。
> go install github.com/mlowicki/lab && ./bin/lab Deferred by h Deferred by g Deferred by f panic: boom! panic: 2nd explosion! goroutine 5 [running]: panic(0xb8480, 0x8201c82d0) /usr/local/go/src/runtime/panic.go:481 +0x3e6 main.h.func2() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:25 +0x65 panic(0xb8480, 0x8201c82c0) /usr/local/go/src/runtime/panic.go:443 +0x4e9 main.h() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:27 +0xa3 main.g() /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:17 +0x35 main.f(0x820214060) /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:9 +0x35 created by main.main /Users/mlowicki/projects/golang/spec/src/github.com/mlowicki/lab/lab.go:32 +0x53
Recover
內建的recover 函式可以檢視 panic 是否被觸發並且阻止 panic 的其他影響。return 語句的返回值要麼是傳遞給panic 的引數(如果正好有 panic ),要麼是nil 。在呼叫 recover 之後,當前 panic 的序列停止,並且這個程式就像,在一個內部延遲的函式呼叫了 recover 函式的函式被呼叫時候,就從未有 panic 發生一樣:
package main import "fmt" func f() { fmt.Println("Start f") defer func() { fmt.Println("Deferred in f") }() g() fmt.Println("End f") } func g() { fmt.Println("Start g") defer func() { fmt.Println("Deferred in g") }() h() fmt.Println("End g") } func h() { fmt.Println("Start h") defer func() { fmt.Println("1st deferred in h") }() defer func() { fmt.Println("2nd deferred in h") if p := recover(); p != nil { fmt.Printf("Panic found: %v\n", p) } }() defer func() { fmt.Println("3rd deferred in h") }() panic("boom!") } func main() { f() }
> go install github.com/mlowicki/lab && ./bin/lab Start f Start g Start h 3rd deferred in h 2nd deferred in h Panic found: boom! 1st deferred in h End g Deferred in g End f Deferred in f
允許在推遲的的函式外呼叫 recover 函式,但總是會返回 nil。
當沒有活躍的 panic 的時候,在推遲的函式中呼叫recover 的返回值是nil 。如果panic 在以nil 為引數的時候被呼叫,就無法判斷 panic 是否正在進行中。
如果你喜歡這個帖子並且想獲得一些新帖子的最新資訊就請關注我吧。讓別人也發現這篇文章請點選下面的小心心。
參考來源
- Concurrency Is Not Parallelism by Rob Pike
- Go specification