如何處理好Golang中的panic和recover

題記

Go 語言自發(fā)布以來,一直以高性能、高并發(fā)著稱。因為標(biāo)準(zhǔn)庫提供了 http 包,即使剛學(xué)不久的程序員,也能輕松寫出 http 服務(wù)程序。

不過,任何事情都有兩面性。一門語言,有它值得驕傲的優(yōu)點(diǎn),也必定隱藏了不少坑。新手若不知道這些坑,很容易就會掉進(jìn)坑里。《 Go 語言踩坑記》系列博文將以 Go 語言中的 panicrecover 開頭,給大家介紹筆者踩過的各種坑,以及填坑方法。

初識 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 ,也沒有 panicrecover 。不過,萬變不離其宗,異常與 if error then return 方式的差別,主要體現(xiàn)在函數(shù)調(diào)用棧的深度上。如下圖:

函數(shù)調(diào)用棧

正常邏輯下的函數(shù)調(diào)用棧,是逐個回溯的,而異常捕獲可以理解為:程序調(diào)用棧的長距離跳轉(zhuǎn)。這點(diǎn)在 C 語言里,是通過 setjumplongjump 這兩個函數(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 catchrecover 、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

源碼

panicrecover 的源碼在 Go 源碼的 src/runtime/panic.go 里,名為 gopanicgorecover 的函數(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)用了 recoverg 的 sp 和 pc
  • 恢復(fù)到 deferrecover 后面的處理邏輯

都有哪些坑

前面提到,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 的 chan 里發(fā)送數(shù)據(jù)
    這也是剛學(xué)用 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.gochansend 函數(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

推薦文章

如何用Go打造千萬級流量秒殺系統(tǒng)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容