題記
Go 語言自發(fā)布以來,一直以高性能、高并發(fā)著稱。因為標(biāo)準(zhǔn)庫提供了 http 包,即使剛學(xué)不久的程序員,也能輕松寫出 http 服務(wù)程序。
不過,任何事情都有兩面性。一門語言,有它值得驕傲的優(yōu)點(diǎn),也必定隱藏了不少坑。新手若不知道這些坑,很容易就會掉進(jìn)坑里。《 Go 語言踩坑記》系列博文將以 Go 語言中的 panic 與 recover 開頭,給大家介紹筆者踩過的各種坑,以及填坑方法。
初識 panic 和 recover
panic
panic 這個詞,在英語中具有恐慌、恐慌的等意思。從字面意思理解的話,在 Go 語言中,代表極其嚴(yán)重的問題,程序員最害怕出現(xiàn)的問題。一旦出現(xiàn),就意味著程序的結(jié)束并退出。Go 語言中 panic 關(guān)鍵字主要用于主動拋出異常,類似 java 等語言中的 throw 關(guān)鍵字。
recover
recover 這個詞,在英語中具有恢復(fù)、復(fù)原等意思。從字面意思理解的話,在 Go 語言中,代表將程序狀態(tài)從嚴(yán)重的錯誤中恢復(fù)到正常狀態(tài)。Go語言中 recover 關(guān)鍵字主要用于捕獲異常,讓程序回到正常狀態(tài),類似 java 等語言中的 try ... catch 。
筆者有過 6 年 linux 系統(tǒng) C 語言開發(fā)經(jīng)歷。C 語言中沒有異常捕獲的概念,沒有 try ... catch ,也沒有 panic 和 recover 。不過,萬變不離其宗,異常與 if error then return 方式的差別,主要體現(xiàn)在函數(shù)調(diào)用棧的深度上。如下圖:
正常邏輯下的函數(shù)調(diào)用棧,是逐個回溯的,而異常捕獲可以理解為:程序調(diào)用棧的長距離跳轉(zhuǎn)。這點(diǎn)在 C 語言里,是通過 setjump 和 longjump 這兩個函數(shù)來實現(xiàn)的。例如以下代碼:
#include <setjmp.h>
#include <stdio.h>
static jmp_buf env;
double divide(double to, double by)
{
if(by == 0)
{
longjmp(env, 1);
}
return to / by;
}
void test_divide()
{
divide(2, 0);
printf("done\n");
}
int main()
{
if (setjmp(env) == 0)
{
test_divide();
}
else
{
printf("Cannot / 0\n");
return -1;
}
return 0;
}
由于發(fā)生了長距離跳轉(zhuǎn),直接從 divide 函數(shù)內(nèi)跳轉(zhuǎn)到 main 函數(shù)內(nèi),中斷了正常的執(zhí)行流,以上代碼編譯后將輸出 Cannot / 0 而不會輸出 done 。是不是很神奇?
try catch 、 recover 、setjump 等機(jī)制會將程序當(dāng)前狀態(tài)(主要是 cpu 的棧指針寄存器 sp 和程序計數(shù)器 pc , Go 的 recover 是依賴 defer 來維護(hù) sp 和 pc )保存到一個與 throw、panic、longjump共享的內(nèi)存里。當(dāng)有異常的時候,從該內(nèi)存中提取之前保存的sp和pc寄存器值,直接將函數(shù)棧調(diào)回到sp指向的位置,并執(zhí)行ip寄存器指向的下一條指令,將程序從異常狀態(tài)中恢復(fù)到正常狀態(tài)。
深入 panic 和 recover
源碼
panic 和 recover 的源碼在 Go 源碼的 src/runtime/panic.go 里,名為 gopanic 和 gorecover 的函數(shù)。
// gopanic 的代碼,在 src/runtime/panic.go 第 454 行
// 預(yù)定義函數(shù) panic 的實現(xiàn)
func gopanic(e interface{}) {
gp := getg()
if gp.m.curg != gp {
print("panic: ")
printany(e)
print("\n")
throw("panic on system stack")
}
if gp.m.mallocing != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic during malloc")
}
if gp.m.preemptoff != "" {
print("panic: ")
printany(e)
print("\n")
print("preempt off reason: ")
print(gp.m.preemptoff)
print("\n")
throw("panic during preemptoff")
}
if gp.m.locks != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic holding locks")
}
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
atomic.Xadd(&runningPanicDefers, 1)
for {
d := gp._defer
if d == nil {
break
}
// 如果觸發(fā) defer 的 panic 是在前一個 panic 或者 Goexit 的 defer 中觸發(fā)的,那么將前一個 defer 從列表中去除。前一個 panic 或者 Goexit 將不再繼續(xù)執(zhí)行。
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
// 將 defer 標(biāo)記為 started,但是保留在列表上,這樣,如果在 reflectcall 開始執(zhí)行 d.fn 之前發(fā)生了堆棧增長或垃圾回收,則 traceback 可以找到并更新 defer 的參數(shù)幀。
d.started = true
// 將正在執(zhí)行 defer 的 panic 保存下來。如果在該 panic 的 defer 函數(shù)中觸發(fā)了新的 panic ,則新 panic 在列表中將會找到 d 并將 d._panic 標(biāo)記為 aborted 。
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
p.argp = nil
// reflectcall 不會 panic,移除 d 。
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
// 這里用 GC() 來觸發(fā)堆棧收縮以測試堆棧拷貝。由于是測試代碼,所以注釋掉了。參考 stack_test.go:TestStackPanic
//GC()
pc := d.pc
sp := unsafe.Pointer(d.sp) // 必須是指針,以便在堆棧復(fù)制期間進(jìn)行調(diào)整
// defer 處理函數(shù)的內(nèi)存是動態(tài)分配的,在執(zhí)行完后需要釋放內(nèi)存。所以,如果 defer 一直得不到執(zhí)行(比如在死循環(huán)中一直創(chuàng)建 defer),將會導(dǎo)致內(nèi)存泄露
freedefer(d)
if p.recovered {
atomic.Xadd(&runningPanicDefers, -1)
gp._panic = p.link
// 已退出的 panic 已經(jīng)被標(biāo)記,但還遺留在 g.panic 列表里,從列表里移除他們。
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // must be done with signal
gp.sig = 0
}
// 將正在恢復(fù)的棧幀傳給 recovery。
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed") // mcall 不應(yīng)該返回
}
}
// 如果所有的 defer 都遍歷完畢,意味著沒有 recover(前面提到,mcall 執(zhí)行 recovery 是不返回的),繼續(xù)執(zhí)行 panic 后續(xù)流程,如:輸出調(diào)用棧信息和錯誤信息
// 由于在凍結(jié)世界之后調(diào)用任意用戶代碼是不安全的,因此我們調(diào)用preprintpanics來調(diào)用所有必要的Error和String方法以在startpanic之前準(zhǔn)備 panic 輸出的字符串。
preprintpanics(gp._panic)
fatalpanic(gp._panic) // 不應(yīng)該返回
*(*int)(nil) = 0 // 因為 fatalpanic 不應(yīng)該返回,正常情況下這里不會執(zhí)行。如果執(zhí)行到了,這行代碼將觸發(fā) panic
}
// gorecover 的代碼,在 src/runtime/panic.go 第 585 行
// 預(yù)定義函數(shù) recover 的實現(xiàn)。
// 無法拆分堆棧,因為它需要可靠地找到其調(diào)用方的堆棧段。
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
// 在處理 panic 的時候,recover 函數(shù)的調(diào)用必須放在 defer 的頂層處理函數(shù)中。
// p.argp 是最頂層的延遲函數(shù)調(diào)用的參數(shù)指針,與調(diào)用方傳遞的argp進(jìn)行比較,如果一致,則該調(diào)用方是可以恢復(fù)的。
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
從函數(shù)代碼中我們可以看到 panic 內(nèi)部主要流程是這樣:
- 獲取當(dāng)前調(diào)用者所在的
g,也就是goroutine - 遍歷并執(zhí)行
g中的defer函數(shù) - 如果
defer函數(shù)中有調(diào)用recover,并發(fā)現(xiàn)已經(jīng)發(fā)生了panic,則將panic標(biāo)記為recovered - 在遍歷
defer的過程中,如果發(fā)現(xiàn)已經(jīng)被標(biāo)記為recovered,則提取出該defer的 sp 與 pc,保存在g的兩個狀態(tài)碼字段中。 - 調(diào)用
runtime.mcall切到m->g0并跳轉(zhuǎn)到recovery函數(shù),將前面獲取的g作為參數(shù)傳給recovery函數(shù)。
runtime.mcall的代碼在 go 源碼的src/runtime/asm_xxx.s中,xxx是平臺類型,如amd64。代碼如下:
// src/runtime/asm_amd64.s 第 274 行
// func mcall(fn func(*g))
// Switch to m->g0's stack, call fn(g).
// Fn must never return. It should gogo(&g->sched)
// to keep running g.
TEXT runtime·mcall(SB), NOSPLIT, $0-8
MOVQ fn+0(FP), DI
get_tls(CX)
MOVQ g(CX), AX // save state in g->sched
MOVQ 0(SP), BX // caller's PC
MOVQ BX, (g_sched+gobuf_pc)(AX)
LEAQ fn+0(FP), BX // caller's SP
MOVQ BX, (g_sched+gobuf_sp)(AX)
MOVQ AX, (g_sched+gobuf_g)(AX)
MOVQ BP, (g_sched+gobuf_bp)(AX)
// switch to m->g0 & its stack, call fn
MOVQ g(CX), BX
MOVQ g_m(BX), BX
MOVQ m_g0(BX), SI
CMPQ SI, AX // if g == m->g0 call badmcall
JNE 3(PC)
MOVQ $runtime·badmcall(SB), AX
JMP AX
MOVQ SI, g(CX) // g = m->g0
MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp
PUSHQ AX
MOVQ DI, DX
MOVQ 0(DI), DI
CALL DI
POPQ AX
MOVQ $runtime·badmcall2(SB), AX
JMP AX
RET
這里之所以要切到 m->g0 ,主要是因為 Go 的 runtime 環(huán)境是有自己的堆棧和 goroutine,而 recovery 是在 runtime 環(huán)境下執(zhí)行的,所以要先調(diào)度到 m->g0 來執(zhí)行 recovery 函數(shù)。
-
recovery函數(shù)中,利用g中的兩個狀態(tài)碼回溯棧指針 sp 并恢復(fù)程序計數(shù)器 pc 到調(diào)度器中,并調(diào)用gogo重新調(diào)度g,將g恢復(fù)到調(diào)用recover函數(shù)的位置, goroutine 繼續(xù)執(zhí)行。
代碼如下:
// gorecover 的代碼,在 src/runtime/panic.go 第 637 行
// 在 panic 后,在延遲函數(shù)中調(diào)用 recover 的時候,將回溯堆棧,并且繼續(xù)執(zhí)行,就像延遲函數(shù)的調(diào)用者正常返回一樣。
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// 延遲函數(shù)的參數(shù)必須已經(jīng)保存在堆棧中了(這里通過判斷 sp 是否處于棧內(nèi)存地址的范圍內(nèi)來保障參數(shù)的正確處理)
if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
throw("bad recovery")
}
// 讓延遲函數(shù)的 deferproc 再次返回,這次返回 1 。調(diào)用函數(shù)將跳轉(zhuǎn)到標(biāo)準(zhǔn)返回結(jié)尾。
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}
// src/runtime/asm_amd64.s 第 274 行
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX
MOVQ 0(DX), CX // make sure g != nil
get_tls(CX)
MOVQ DX, g(CX)
MOVQ gobuf_sp(BX), SP // 從 gobuf 中恢復(fù) SP ,以便后面做跳轉(zhuǎn)
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
MOVQ $0, gobuf_sp(BX) // 這里開始清理 gobuf ,以便垃圾回收。
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX // 從 gobuf 中恢復(fù) pc ,以便跳轉(zhuǎn)
JMP BX
以上便是 Go 底層處理異常的流程,精簡為三步便是:
-
defer函數(shù)中調(diào)用recover - 觸發(fā)
panic并切到runtime環(huán)境獲取在defer中調(diào)用了recover的g的 sp 和 pc - 恢復(fù)到
defer中recover后面的處理邏輯
都有哪些坑
前面提到,panic 函數(shù)主要用于主動觸發(fā)異常。我們在實現(xiàn)業(yè)務(wù)代碼的時候,在程序啟動階段,如果資源初始化出錯,可以主動調(diào)用 panic 立即結(jié)束程序。對于新手來說,這沒什么問題,很容易做到。
但是,現(xiàn)實往往是殘酷的—— Go 的 runtime 代碼中很多地方都調(diào)用了 panic 函數(shù),對于不了解 Go 底層實現(xiàn)的新人來說,這無疑是挖了一堆深坑。如果不熟悉這些坑,是不可能寫出健壯的 Go 代碼。
接下來,筆者給大家細(xì)數(shù)下都有哪些坑。
-
數(shù)組( slice )下標(biāo)越界
這個比較好理解,對于靜態(tài)類型語言,數(shù)組下標(biāo)越界是致命錯誤。如下代碼可以驗證:
package main
import (
"fmt"
)
func foo(){
defer func(){
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var bar = []int{1}
fmt.Println(bar[1])
}
func main(){
foo()
fmt.Println("exit")
}
輸出:
runtime error: index out of range
exit
因為代碼中用了 recover ,程序得以恢復(fù),輸出 exit。
如果將 recover 那幾行注釋掉,將會輸出如下日志:
panic: runtime error: index out of range
goroutine 1 [running]:
main.foo()
/home/letian/work/go/src/test/test.go:14 +0x3e
main.main()
/home/letian/work/go/src/test/test.go:18 +0x22
exit status 2
-
訪問未初始化的指針或 nil 指針
對于有 c/c++ 開發(fā)經(jīng)驗的人來說,這個很好理解。但對于沒用過指針的新手來說,這是最常見的一類錯誤。
如下代碼可以驗證:
package main
import (
"fmt"
)
func foo(){
defer func(){
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var bar *int
fmt.Println(*bar)
}
func main(){
foo()
fmt.Println("exit")
}
輸出:
runtime error: invalid memory address or nil pointer dereference
exit
如果將 recover 那幾行代碼注釋掉,則會輸出:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4869ff]
goroutine 1 [running]:
main.foo()
/home/letian/work/go/src/test/test.go:14 +0x3f
main.main()
/home/letian/work/go/src/test/test.go:18 +0x22
exit status 2
-
試圖往已經(jīng) close 的
這也是剛學(xué)用chan里發(fā)送數(shù)據(jù)chan的新手容易犯的錯誤。如下代碼可以驗證:
package main
import (
"fmt"
)
func foo(){
defer func(){
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var bar = make(chan int, 1)
close(bar)
bar<-1
}
func main(){
foo()
fmt.Println("exit")
}
輸出:
send on closed channel
exit
如果注釋掉 recover ,將輸出:
panic: send on closed channel
goroutine 1 [running]:
main.foo()
/home/letian/work/go/src/test/test.go:15 +0x83
main.main()
/home/letian/work/go/src/test/test.go:19 +0x22
exit status 2
源碼處理邏輯在 src/runtime/chan.go 的 chansend 函數(shù)中,如下圖:
// src/runtime/chan.go 第 269 行
// 如果 block 不為 nil ,則協(xié)議將不會休眠,但如果無法完成則返回。
// 當(dāng)關(guān)閉休眠中的通道時,可以使用 g.param == nil 喚醒睡眠。
// 我們可以非常容易循環(huán)并重新運(yùn)行該操作,并且將會看到它處于已關(guān)閉狀態(tài)。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not closed, we observe that the channel is
// not ready for sending. Each of these observations is a single word-sized read
// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
// Because a closed channel cannot transition from 'ready for sending' to
// 'not ready for sending', even if the channel is closed between the two observations,
// they imply a moment between the two when the channel was both not yet closed
// and not ready for sending. We behave as if we observed the channel at that moment,
// and report that the send cannot proceed.
//
// It is okay if the reads are reordered here: if we observe that the channel is not
// ready for sending and then observe that it is not closed, that implies that the
// channel wasn't closed during the first observation.
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
if !block {
unlock(&c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
KeepAlive(ep)
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true
}
-
并發(fā)讀寫相同 map
對于剛學(xué)并發(fā)編程的同學(xué)來說,并發(fā)讀寫 map 也是很容易遇到的問題。如下代碼可以驗證:
package main
import (
"fmt"
)
func foo(){
defer func(){
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var bar = make(map[int]int)
go func(){
defer func(){
if err := recover(); err != nil {
fmt.Println(err)
}
}()
for{
_ = bar[1]
}
}()
for{
bar[1]=1
}
}
func main(){
foo()
fmt.Println("exit")
}
輸出:
fatal error: concurrent map read and map write
goroutine 5 [running]:
runtime.throw(0x4bd8b0, 0x21)
/home/letian/.gvm/gos/go1.12/src/runtime/panic.go:617 +0x72 fp=0xc00004c780 sp=0xc00004c750 pc=0x427f22
runtime.mapaccess1_fast64(0x49eaa0, 0xc000088180, 0x1, 0xc0000260d8)
/home/letian/.gvm/gos/go1.12/src/runtime/map_fast64.go:21 +0x1a8 fp=0xc00004c7a8 sp=0xc00004c780 pc=0x40eb58
main.foo.func2(0xc000088180)
/home/letian/work/go/src/test/test.go:21 +0x5c fp=0xc00004c7d8 sp=0xc00004c7a8 pc=0x48708c
runtime.goexit()
/home/letian/.gvm/gos/go1.12/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc00004c7e0 sp=0xc00004c7d8 pc=0x450e51
created by main.foo
/home/letian/work/go/src/test/test.go:14 +0x68
goroutine 1 [runnable]:
main.foo()
/home/letian/work/go/src/test/test.go:25 +0x8b
main.main()
/home/letian/work/go/src/test/test.go:30 +0x22
exit status 2
細(xì)心的朋友不難發(fā)現(xiàn),輸出日志里沒有出現(xiàn)我們在程序末尾打印的 exit,而是直接將調(diào)用棧打印出來了。查看 src/runtime/map.go 中的代碼不難發(fā)現(xiàn)這幾行:
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
與前面提到的幾種情況不同,runtime 中調(diào)用 throw 函數(shù)拋出的異常是無法在業(yè)務(wù)代碼中通過 recover 捕獲的,這點(diǎn)最為致命。所以,對于并發(fā)讀寫 map 的地方,應(yīng)該對 map 加鎖。
-
類型斷言
在使用類型斷言對interface進(jìn)行類型轉(zhuǎn)換的時候也容易一不小心踩坑,而且這個坑是即使用interface有一段時間的人也容易忽略的問題。如下代碼可以驗證:
package main
import (
"fmt"
)
func foo(){
defer func(){
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var i interface{} = "abc"
_ = i.([]string)
}
func main(){
foo()
fmt.Println("exit")
}
輸出:
interface conversion: interface {} is string, not []string
exit
源碼在 src/runtime/iface.go 中,如下兩個函數(shù):
// panicdottypeE is called when doing an e.(T) conversion and the conversion fails.
// have = the dynamic type we have.
// want = the static type we're trying to convert to.
// iface = the static type we're converting from.
func panicdottypeE(have, want, iface *_type) {
panic(&TypeAssertionError{iface, have, want, ""})
}
// panicdottypeI is called when doing an i.(T) conversion and the conversion fails.
// Same args as panicdottypeE, but "have" is the dynamic itab we have.
func panicdottypeI(have *itab, want, iface *_type) {
var t *_type
if have != nil {
t = have._type
}
panicdottypeE(t, want, iface)
}
更多的 panic
前面提到的只是基本語法中常遇到的幾種 panic 場景,Go 標(biāo)準(zhǔn)庫中有更多使用 panic 的地方,大家可以在源碼中搜索 panic( 找到調(diào)用的地方,以免后續(xù)用標(biāo)準(zhǔn)庫函數(shù)的時候踩坑。
限于篇幅,本文暫不介紹填坑技巧,后面再開其他篇幅逐個介紹。
感謝閱讀!
下回預(yù)告
Go語言踩坑記之channel與goroutine