亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

淺析Go中函數(shù)的健壯性,panic異常處理和defer機(jī)制

 更新時間:2023年10月20日 08:03:37   作者:賈維斯Echo  
這篇文章主要為大家詳細(xì)介紹了Go中函數(shù)的健壯性,panic異常處理和defer機(jī)制的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下

一、函數(shù)健壯性的“三不要”原則

1.1 原則一:不要相信任何外部輸入的參數(shù)

函數(shù)的使用者可能是任何人,這些人在使用函數(shù)之前可能都沒有閱讀過任何手冊或文檔,他們會向函數(shù)傳入你意想不到的參數(shù)。因此,為了保證函數(shù)的健壯性,函數(shù)需要對所有輸入的參數(shù)進(jìn)行合法性的檢查。一旦發(fā)現(xiàn)問題,立即終止函數(shù)的執(zhí)行,返回預(yù)設(shè)的錯誤值。

1.2 原則二:不要忽略任何一個錯誤

在我們的函數(shù)實(shí)現(xiàn)中,也會調(diào)用標(biāo)準(zhǔn)庫或第三方包提供的函數(shù)或方法。對于這些調(diào)用,我們不能假定它一定會成功,我們一定要顯式地檢查這些調(diào)用返回的錯誤值。一旦發(fā)現(xiàn)錯誤,要及時終止函數(shù)執(zhí)行,防止錯誤繼續(xù)傳播。

1.3 原則三:不要假定異常不會發(fā)生

這里,我們先要確定一個認(rèn)知:異常不是錯誤。錯誤是可預(yù)期的,也是經(jīng)常會發(fā)生的,我們有對應(yīng)的公開錯誤碼和錯誤處理預(yù)案,但異常卻是少見的、意料之外的。通常意義上的異常,指的是硬件異常、操作系統(tǒng)異常、語言運(yùn)行時異常,還有更大可能是代碼中潛在 bug 導(dǎo)致的異常,比如代碼中出現(xiàn)了以 0 作為分母,或者是數(shù)組越界訪問等情況。

雖然異常發(fā)生是“小眾事件”,但是我們不能假定異常不會發(fā)生。所以,函數(shù)設(shè)計時,我們就需要根據(jù)函數(shù)的角色和使用場景,考慮是否要在函數(shù)內(nèi)設(shè)置異常捕捉和恢復(fù)的環(huán)節(jié)。

二、Go 語言中的異常:panic

2.1 panic 異常處理介紹

不同編程語言表示異常(Exception)這個概念的語法都不相同。在 Go 語言中,異常這個概念由 panic 表示。

panic 指的是 Go 程序在運(yùn)行時出現(xiàn)的一個異常情況。如果異常出現(xiàn)了,但沒有被捕獲并恢復(fù),Go 程序的執(zhí)行就會被終止,即便出現(xiàn)異常的位置不在主 Goroutine 中也會這樣。

在 Go 中,panic 主要有兩類來源,一類是來自 Go 運(yùn)行時,另一類則是 Go 開發(fā)人員通過 panic 函數(shù)主動觸發(fā)的。無論是哪種,一旦 panic 被觸發(fā),后續(xù) Go 程序的執(zhí)行過程都是一樣的,這個過程被 Go 語言稱為 panicking

2.2 panicking 的過程

Go 官方文檔以手工調(diào)用 panic 函數(shù)觸發(fā) panic 為例,對 panicking 這個過程進(jìn)行了詮釋:當(dāng)函數(shù) F 調(diào)用 panic 函數(shù)時,函數(shù) F 的執(zhí)行將停止。不過,函數(shù) F 中已進(jìn)行求值的 deferred 函數(shù)都會得到正常執(zhí)行,執(zhí)行完這些 deferred 函數(shù)后,函數(shù) F 才會把控制權(quán)返還給其調(diào)用者。

對于函數(shù) F 的調(diào)用者而言,函數(shù) F 之后的行為就如同調(diào)用者調(diào)用的函數(shù)是 panic 一樣,該 panicking 過程將繼續(xù)在棧上進(jìn)行下去,直到當(dāng)前 Goroutine 中的所有函數(shù)都返回為止,然后 Go 程序?qū)⒈罎⑼顺觥?/p>

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

下面,我們用一個例子來更直觀地解釋一下 panicking 這個過程:

func foo() {
    println("call foo")
    bar()
    println("exit foo")
}

func bar() {
    println("call bar")
    panic("panic occurs in bar")
    zoo()
    println("exit bar")
}

func zoo() {
    println("call zoo")
    println("exit zoo")
}

func main() {
    println("call main")
    foo()
    println("exit main")
}

上面這個例子中,從 Go 應(yīng)用入口開始,函數(shù)的調(diào)用次序依次為 main -> foo -> bar -> zoo。在 bar 函數(shù)中,我們調(diào)用 panic 函數(shù)手動觸發(fā)了 panic。

我們執(zhí)行這個程序的輸出結(jié)果是這樣的:

call main
call foo
call bar
panic: panic occurs in bar

根據(jù)前面對 panicking 過程的詮釋,理解一下這個例子。

這里,程序從入口函數(shù) main 開始依次調(diào)用了 foo、bar 函數(shù),在 bar 函數(shù)中,代碼在調(diào)用 zoo 函數(shù)之前調(diào)用了 panic 函數(shù)觸發(fā)了異常。那示例的 panicking 過程就從這開始了。bar 函數(shù)調(diào)用 panic 函數(shù)之后,它自身的執(zhí)行就此停止了,所以我們也沒有看到代碼繼續(xù)進(jìn)入 zoo 函數(shù)執(zhí)行。并且,bar 函數(shù)沒有捕捉這個 panic,這樣這個 panic 就會沿著函數(shù)調(diào)用棧向上走,來到了 bar 函數(shù)的調(diào)用者 foo 函數(shù)中。

從 foo 函數(shù)的視角來看,這就好比將它對 bar 函數(shù)的調(diào)用,換成了對 panic 函數(shù)的調(diào)用一樣。這樣一來,foo 函數(shù)的執(zhí)行也被停止了。由于 foo 函數(shù)也沒有捕捉 panic,于是 panic 繼續(xù)沿著函數(shù)調(diào)用棧向上走,來到了 foo 函數(shù)的調(diào)用者 main 函數(shù)中。

同理,從 main 函數(shù)的視角來看,這就好比將它對 foo 函數(shù)的調(diào)用,換成了對 panic 函數(shù)的調(diào)用一樣。結(jié)果就是,main 函數(shù)的執(zhí)行也被終止了,于是整個程序異常退出,日志"exit main"也沒有得到輸出的機(jī)會。

2.3 recover 函數(shù)介紹

recover 是Go語言中的一個內(nèi)置函數(shù),用于在發(fā)生 panic 時捕獲并處理 panic,以便程序能夠繼續(xù)執(zhí)行而不會完全崩潰。以下是有關(guān) recover 函數(shù)的介紹:

  • 用途recover 用于恢復(fù)程序的控制權(quán),防止程序因 panic 而崩潰。它通常與 defer 一起使用,用于在發(fā)生異常情況時執(zhí)行一些清理操作、記錄錯誤信息或者嘗試恢復(fù)程序狀態(tài)。
  • 工作原理:當(dāng)程序進(jìn)入 panic 狀態(tài)時,recover 可以用來停止 panic 的傳播。它會返回導(dǎo)致 panic 的值(通常是一個錯誤信息),允許程序捕獲這個值并采取適當(dāng)?shù)拇胧H绻?nbsp;recover 在當(dāng)前函數(shù)內(nèi)沒有找到可捕獲的 panic,它會返回 nil。
  • 與 panic 配合使用:通常,recover 會與 defer 一起使用。在 defer 中使用 recover,可以確保在函數(shù)返回之前檢查 panic 狀態(tài)并采取適當(dāng)?shù)拇胧?/li>
  • 局限性recover 只能用于捕獲最近一次的 panic,它不能用于捕獲之前的 panic。一旦 recover 成功捕獲了一個 panic,它會重置 panic 狀態(tài),因此無法繼續(xù)捕獲之前的 panic。

接著,我們繼續(xù)用上面這個例子分析,在觸發(fā) panic 的 bar 函數(shù)中,對 panic 進(jìn)行捕捉并恢復(fù),我們直接來看恢復(fù)后,整個程序的執(zhí)行情況是什么樣的。這里,我們只列出了變更后的 bar 函數(shù)代碼,其他函數(shù)代碼并沒有改變,代碼如下:

package main

import "fmt"

func foo() {
	println("call foo")
	bar()
	println("exit foo")
}

//	func bar() {
//		println("call bar")
//		panic("panic occurs in bar")
//		zoo()
//		println("exit bar")
//	}
func bar() {
	defer func() {
		if e := recover(); e != nil {
			fmt.Println("recover the panic:", e)
		}
	}()

	println("call bar")
	panic("panic occurs in bar")
	zoo()
	println("exit bar")
}

func zoo() {
	println("call zoo")
	println("exit zoo")
}

func main() {
	println("call main")
	foo()
	println("exit main")
}

在更新版的 bar 函數(shù)中,我們在一個 defer 匿名函數(shù)中調(diào)用 recover 函數(shù)對 panic 進(jìn)行了捕捉。recover 是 Go 內(nèi)置的專門用于恢復(fù) panic 的函數(shù),它必須被放在一個 defer 函數(shù)中才能生效。如果 recover 捕捉到 panic,它就會返回以 panic 的具體內(nèi)容為錯誤上下文信息的錯誤值。如果沒有 panic 發(fā)生,那么 recover 將返回 nil。而且,如果 panic 被 recover 捕捉到,panic 引發(fā)的 panicking 過程就會停止。

我們執(zhí)行更新后的程序,得到如下結(jié)果:

call main
call foo
call bar
recover the panic: panic occurs in bar
exit foo
exit main

我們可以看到 main 函數(shù)終于得以“善終”。那這個過程中究竟發(fā)生了什么呢?

在更新后的代碼中,當(dāng) bar 函數(shù)調(diào)用 panic 函數(shù)觸發(fā)異常后,bar 函數(shù)的執(zhí)行就會被中斷。但這一次,在代碼執(zhí)行流回到 bar 函數(shù)調(diào)用者之前,bar 函數(shù)中的、在 panic 之前就已經(jīng)被設(shè)置成功的 derfer 函數(shù)就會被執(zhí)行。這個匿名函數(shù)會調(diào)用 recover 把剛剛觸發(fā)的 panic 恢復(fù),這樣,panic 還沒等沿著函數(shù)棧向上走,就被消除了。

所以,這個時候,從 foo 函數(shù)的視角來看,bar 函數(shù)與正常返回沒有什么差別。foo 函數(shù)依舊繼續(xù)向下執(zhí)行,直至 main 函數(shù)成功返回。這樣,這個程序的 panic“危機(jī)”就解除了。

面對有如此行為特點(diǎn)的 panic,我們應(yīng)該如何應(yīng)對呢?是不是在所有 Go 函數(shù)或方法中,我們都要用 defer 函數(shù)來捕捉和恢復(fù) panic 呢?

三、如何應(yīng)對 panic

其實(shí)大可不必。一來,這樣做會徒增開發(fā)人員函數(shù)實(shí)現(xiàn)時的心智負(fù)擔(dān)。二來,很多函數(shù)非常簡單,根本不會出現(xiàn) panic 情況,我們增加 panic 捕獲和恢復(fù),反倒會增加函數(shù)的復(fù)雜性。同時,defer 函數(shù)也不是“免費(fèi)”的,也有帶來性能開銷。

日常情況下,我們應(yīng)該采取以下3點(diǎn)經(jīng)驗(yàn)。

3.1 第一點(diǎn):評估程序?qū)?panic 的忍受度

首先,我們應(yīng)該知道一個事實(shí):不同應(yīng)用對異常引起的程序崩潰退出的忍受度是不一樣的。比如,一個單次運(yùn)行于控制臺窗口中的命令行交互類程序(CLI),和一個常駐內(nèi)存的后端 HTTP 服務(wù)器程序,對異常崩潰的忍受度就是不同的。

前者即便因異常崩潰,對用戶來說也僅僅是再重新運(yùn)行一次而已。但后者一旦崩潰,就很可能導(dǎo)致整個網(wǎng)站停止服務(wù)。所以,針對各種應(yīng)用對 panic 忍受度的差異,我們采取的應(yīng)對 panic 的策略也應(yīng)該有不同。像后端 HTTP 服務(wù)器程序這樣的任務(wù)關(guān)鍵系統(tǒng),我們就需要在特定位置捕捉并恢復(fù) panic,以保證服務(wù)器整體的健壯度。在這方面,Go 標(biāo)準(zhǔn)庫中的 http server 就是一個典型的代表。

Go 標(biāo)準(zhǔn)庫提供的 http server 采用的是,每個客戶端連接都使用一個單獨(dú)的 Goroutine 進(jìn)行處理的并發(fā)處理模型。也就是說,客戶端一旦與 http server 連接成功,http server 就會為這個連接新創(chuàng)建一個 Goroutine,并在這 Goroutine 中執(zhí)行對應(yīng)連接(conn)的 serve 方法,來處理這條連接上的客戶端請求。

前面提到了 panic 的“危害”時,我們說過,無論在哪個 Goroutine 中發(fā)生未被恢復(fù)的 panic,整個程序都將崩潰退出。所以,為了保證處理某一個客戶端連接的 Goroutine 出現(xiàn) panic 時,不影響到 http server 主 Goroutine 的運(yùn)行,Go 標(biāo)準(zhǔn)庫在 serve 方法中加入了對 panic 的捕捉與恢復(fù),下面是 serve 方法的部分代碼片段:

// $GOROOT/src/net/http/server.go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    defer func() {
        if err := recover(); err != nil && err != ErrAbortHandler {
            const size = 64 << 10
            buf := make([]byte, size)
            buf = buf[:runtime.Stack(buf, false)]
            c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
        }
        if !c.hijacked() {
            c.close()
            c.setState(c.rwc, StateClosed, runHooks)
        }
    }()
    ... ...
}

可以看到,serve 方法在一開始處就設(shè)置了 defer 函數(shù),并在該函數(shù)中捕捉并恢復(fù)了可能出現(xiàn)的 panic。這樣,即便處理某個客戶端連接的 Goroutine 出現(xiàn) panic,處理其他連接 Goroutine 以及 http server 自身都不會受到影響。

這種局部不要影響整體的異常處理策略,在很多并發(fā)程序中都有應(yīng)用。并且,捕捉和恢復(fù) panic 的位置通常都在子 Goroutine 的起始處,這樣設(shè)置可以捕捉到后面代碼中可能出現(xiàn)的所有 panic,就像 serve 方法中那樣。

3.2 第二點(diǎn):提示潛在 bug

有了對 panic 忍受度的評估,panic 也沒有那么“恐怖”,而且,我們甚至可以借助 panic 來幫助我們快速找到潛在 bug。

Go 語言標(biāo)準(zhǔn)庫中并沒有提供斷言之類的輔助函數(shù),但我們可以使用 panic,部分模擬斷言對潛在 bug 的提示功能。比如,下面就是標(biāo)準(zhǔn)庫 encoding/json包使用 panic 指示潛在 bug 的一個例子:

// $GOROOT/src/encoding/json/decode.go
... ...
//當(dāng)一些本不該發(fā)生的事情導(dǎo)致我們結(jié)束處理時,phasePanicMsg將被用作panic消息
//它可以指示JSON解碼器中的bug,或者
//在解碼器執(zhí)行時還有其他代碼正在修改數(shù)據(jù)切片。

const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?"

func (d *decodeState) init(data []byte) *decodeState {
    d.data = data
    d.off = 0
    d.savedError = nil
    if d.errorContext != nil {
        d.errorContext.Struct = nil
        // Reuse the allocated space for the FieldStack slice.
        d.errorContext.FieldStack = d.errorContext.FieldStack[:0]
    }
    return d
}

func (d *decodeState) valueQuoted() interface{} {
    switch d.opcode {
    default:
        panic(phasePanicMsg)

    case scanBeginArray, scanBeginObject:
        d.skip()
        d.scanNext()

    case scanBeginLiteral:
        v := d.literalInterface()
        switch v.(type) {
        case nil, string:
            return v
        }
    }
    return unquotedValue{}
}

我們看到,在 valueQuoted 這個方法中,如果程序執(zhí)行流進(jìn)入了 default 分支,那這個方法就會引發(fā) panic,這個 panic 會提示開發(fā)人員:這里很可能是一個 bug。

同樣,在 json 包的 encode.go 中也有使用 panic 做潛在 bug 提示的例子:

// $GOROOT/src/encoding/json/encode.go

func (w *reflectWithString) resolve() error {
    ... ...
    switch w.k.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        w.ks = strconv.FormatInt(w.k.Int(), 10)
        return nil
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        w.ks = strconv.FormatUint(w.k.Uint(), 10)
        return nil
    }
    panic("unexpected map key type")
}

這段代碼中,resolve 方法的最后一行代碼就相當(dāng)于一個“代碼邏輯不會走到這里”的斷言。一旦觸發(fā)“斷言”,這很可能就是一個潛在 bug。

我們也看到,去掉這行代碼并不會對 resolve 方法的邏輯造成任何影響,但真正出現(xiàn)問題時,開發(fā)人員就缺少了“斷言”潛在 bug 提醒的輔助支持了。在 Go 標(biāo)準(zhǔn)庫中,大多數(shù) panic 的使用都是充當(dāng)類似斷言的作用的。

3.3 第三點(diǎn):不要混淆異常與錯誤

在日常編碼中,一些 Go 語言初學(xué)者,尤其是一些有過Python,Java等語言編程經(jīng)驗(yàn)的程序員,會因?yàn)榱?xí)慣了 Python 那種基于try-except 的錯誤處理思維,而將 Go panic 當(dāng)成Python 的“checked exception”去用,這顯然是混淆了 Go 中的異常與錯誤,這是 Go 錯誤處理的一種反模式。

查看Python 標(biāo)準(zhǔn)類庫,我們可以看到一些 Java 已預(yù)定義好的 checked exception 類,比較常見的有ValueErrorTypeError等等??吹竭@里,這些 checked exception 都是預(yù)定義好的、代表特定場景下的錯誤狀態(tài)。

那 Python 的 checked exception 和 Go 中的 panic 有啥差別呢?

Python 的 checked exception 用于一些可預(yù)見的、常會發(fā)生的錯誤場景,比如,針對 checked exception 的所謂“異常處理”,就是針對這些場景的“錯誤處理預(yù)案”。也可以說對 checked exception 的使用、捕獲、自定義等行為都是“有意而為之”的。

如果它非要和 Go 中的某種語法對應(yīng)來看,它對應(yīng)的也是 Go 的錯誤處理,也就是基于 error 值比較模型的錯誤處理。所以,Python 中對 checked exception 處理的本質(zhì)是錯誤處理,雖然它的名字用了帶有“異常”的字樣。

而 Go 中的 panic 呢,更接近于 Python 的 RuntimeException,而不是 checked exception 。我們前面提到過 Python 的 checked exception 是必須要被上層代碼處理的,也就是要么捕獲處理,要么重新拋給更上層。但是在 Go 中,我們通常會導(dǎo)入大量第三方包,而對于這些第三方包 API 中是否會引發(fā) panic ,我們是不知道的。

因此上層代碼,也就是 API 調(diào)用者根本不會去逐一了解 API 是否會引發(fā)panic,也沒有義務(wù)去處理引發(fā)的 panic。一旦你在編寫的 API 中,像 checked exception 那樣使用 panic 作為正常錯誤處理的手段,把引發(fā)的 panic 當(dāng)作錯誤,那么你就會給你的 API 使用者帶去大麻煩!因此,在 Go 中,作為 API 函數(shù)的作者,你一定不要將 panic 當(dāng)作錯誤返回給 API 調(diào)用者。

四、defer 函數(shù)

在Go語言中,defer 是一種用于延遲執(zhí)行函數(shù)或方法調(diào)用的機(jī)制。它通常用于執(zhí)行清理操作、資源釋放、日志記錄等,以確保在函數(shù)返回之前進(jìn)行這些操作。下面是有關(guān) defer 函數(shù)的介紹和如何使用它來簡化函數(shù)實(shí)現(xiàn)的內(nèi)容:

4.1 defer 函數(shù)介紹

  • 延遲執(zhí)行defer 允許將一個函數(shù)或方法調(diào)用推遲到當(dāng)前函數(shù)返回之前執(zhí)行,無論是正常返回還是由于 panic 引起的異常返回。
  • 執(zhí)行順序:多個 defer 語句按照后進(jìn)先出(LIFO)的順序執(zhí)行,即最后一個注冊的 defer 最先執(zhí)行,倒數(shù)第二個注冊的 defer 在其后執(zhí)行,以此類推。
  • 常見用途defer 常用于資源管理,例如文件關(guān)閉、互斥鎖的釋放、數(shù)據(jù)庫連接的關(guān)閉等,也用于執(zhí)行一些必要的清理工作或日志記錄。
  • 不僅限于函數(shù)調(diào)用defer 不僅可以用于函數(shù)調(diào)用,還可以用于方法調(diào)用,匿名函數(shù)的執(zhí)行等。

4.2 使用 defer 簡化函數(shù)實(shí)現(xiàn)

對函數(shù)設(shè)計來說,如何實(shí)現(xiàn)簡潔的目標(biāo)是一個大話題。你可以從通用的設(shè)計原則去談,比如函數(shù)要遵守單一職責(zé),職責(zé)單一的函數(shù)肯定要比擔(dān)負(fù)多種職責(zé)的函數(shù)更簡單。你也可以從函數(shù)實(shí)現(xiàn)的規(guī)模去談,比如函數(shù)體的規(guī)模要小,盡量控制在 80 行代碼之內(nèi)等。

Go 中提供了defer可以幫助我們簡化 Go 函數(shù)的設(shè)計和實(shí)現(xiàn)。我們用一個具體的例子來理解一下。日常開發(fā)中,我們經(jīng)常會編寫一些類似下面示例中的偽代碼:

func doSomething() error {
    var mu sync.Mutex
    mu.Lock()

    r1, err := OpenResource1()
    if err != nil {
        mu.Unlock()
        return err
    }

    r2, err := OpenResource2()
    if err != nil {
        r1.Close()
        mu.Unlock()
        return err
    }

    r3, err := OpenResource3()
    if err != nil {
        r2.Close()
        r1.Close()
        mu.Unlock()
        return err
    }

    // 使用r1,r2, r3
    err = doWithResources() 
    if err != nil {
        r3.Close()
        r2.Close()
        r1.Close()
        mu.Unlock()
        return err
    }

    r3.Close()
    r2.Close()
    r1.Close()
    mu.Unlock()
    return nil
}

我們看到,這類代碼的特點(diǎn)就是在函數(shù)中會申請一些資源,并在函數(shù)退出前釋放或關(guān)閉這些資源,比如這里的互斥鎖 mu 以及資源 r1~r3 就是這樣。

函數(shù)的實(shí)現(xiàn)需要確保,無論函數(shù)的執(zhí)行流是按預(yù)期順利進(jìn)行,還是出現(xiàn)錯誤,這些資源在函數(shù)退出時都要被及時、正確地釋放。為此,我們需要尤為關(guān)注函數(shù)中的錯誤處理,在錯誤處理時不能遺漏對資源的釋放。

但這樣的要求,就導(dǎo)致我們在進(jìn)行資源釋放,尤其是有多個資源需要釋放的時候,比如上面示例那樣,會大大增加開發(fā)人員的心智負(fù)擔(dān)。同時當(dāng)待釋放的資源個數(shù)較多時,整個代碼邏輯就會變得十分復(fù)雜,程序可讀性、健壯性也會隨之下降。但即便如此,如果函數(shù)實(shí)現(xiàn)中的某段代碼邏輯拋出 panic,傳統(tǒng)的錯誤處理機(jī)制依然沒有辦法捕獲它并嘗試從 panic 恢復(fù)。

Go 語言引入 defer 的初衷,就是解決這些問題。那么,defer 具體是怎么解決這些問題的呢?或者說,defer 具體的運(yùn)作機(jī)制是怎樣的呢?

defer 是 Go 語言提供的一種延遲調(diào)用機(jī)制,defer 的運(yùn)作離不開函數(shù)。怎么理解呢?這句話至少有以下兩點(diǎn)含義:

  • 在 Go 中,只有在函數(shù)(和方法)內(nèi)部才能使用 defer;
  • defer 關(guān)鍵字后面只能接函數(shù)(或方法),這些函數(shù)被稱為 deferred 函數(shù)。defer 將它們注冊到其所在 Goroutine 中,用于存放 deferred 函數(shù)的棧數(shù)據(jù)結(jié)構(gòu)中,這些 deferred 函數(shù)將在執(zhí)行 defer 的函數(shù)退出前,按后進(jìn)先出(LIFO)的順序被程序調(diào)度執(zhí)行(如下圖所示)。

而且,無論是執(zhí)行到函數(shù)體尾部返回,還是在某個錯誤處理分支顯式 return,又或是出現(xiàn) panic,已經(jīng)存儲到 deferred 函數(shù)棧中的函數(shù),都會被調(diào)度執(zhí)行。所以說,deferred 函數(shù)是一個可以在任何情況下為函數(shù)進(jìn)行收尾工作的好“伙伴”。

我們回到剛才的那個例子,如果我們把收尾工作挪到 deferred 函數(shù)中,那么代碼將變成如下這個樣子:

func doSomething() error {
    var mu sync.Mutex
    mu.Lock()
    defer mu.Unlock()

    r1, err := OpenResource1()
    if err != nil {
        return err
    }
    defer r1.Close()

    r2, err := OpenResource2()
    if err != nil {
        return err
    }
    defer r2.Close()

    r3, err := OpenResource3()
    if err != nil {
        return err
    }
    defer r3.Close()

    // 使用r1,r2, r3
    return doWithResources() 
}

我們看到,使用 defer 后對函數(shù)實(shí)現(xiàn)邏輯的簡化是顯而易見的。而且,這里資源釋放函數(shù)的 defer 注冊動作,緊鄰著資源申請成功的動作,這樣成對出現(xiàn)的慣例就極大降低了遺漏資源釋放的可能性,我們開發(fā)人員也不用再小心翼翼地在每個錯誤處理分支中檢查是否遺漏了某個資源的釋放動作。同時,代碼的簡化也意味代碼可讀性的提高,以及代碼健壯度的增強(qiáng)。

五、defer 使用的幾個注意事項(xiàng)

大多數(shù) Gopher 都喜歡 defer,因?yàn)樗粌H可以用來捕捉和恢復(fù) panic,還能讓函數(shù)變得更簡潔和健壯。但“工欲善其事,必先利其器“,一旦你要用 defer,有幾個關(guān)于 defer 使用的注意事項(xiàng)是你一定要提前了解清楚的,可以避免掉進(jìn)一些不必要的“坑”。

5.1 第一點(diǎn):明確哪些函數(shù)可以作為 deferred 函數(shù)

這里,你要清楚,對于自定義的函數(shù)或方法,defer 可以給與無條件的支持,但是對于有返回值的自定義函數(shù)或方法,返回值會在 deferred 函數(shù)被調(diào)度執(zhí)行的時候被自動丟棄。

而且,Go 語言中除了自定義函數(shù) / 方法,還有 Go 語言內(nèi)置的 / 預(yù)定義的函數(shù),這里我給出了 Go 語言內(nèi)置函數(shù)的完全列表:

Functions:
  append cap close complex copy delete imag len
  make new panic print println real recover

那么,Go 語言中的內(nèi)置函數(shù)是否都能作為 deferred 函數(shù)呢?我們看下面的示例:

// defer1.go

 func bar() (int, int) {
     return 1, 2
 }
 
 func foo() {
     var c chan int
     var sl []int
     var m = make(map[string]int, 10)
     m["item1"] = 1
     m["item2"] = 2
     var a = complex(1.0, -1.4)
 
     var sl1 []int
     defer bar()
     defer append(sl, 11)
     defer cap(sl)
     defer close(c)
     defer complex(2, -2)
     defer copy(sl1, sl)
     defer delete(m, "item2")
     defer imag(a)
     defer len(sl)
     defer make([]int, 10)
     defer new(*int)
     defer panic(1)
     defer print("hello, defer\n")
     defer println("hello, defer")
     defer real(a)
     defer recover()
 }
 
 func main() {
     foo()
 }

運(yùn)行這個示例代碼,我們可以得到:

$go run defer1.go
# command-line-arguments
./defer1.go:17:2: defer discards result of append(sl, 11)
./defer1.go:18:2: defer discards result of cap(sl)
./defer1.go:20:2: defer discards result of complex(2, -2)
./defer1.go:23:2: defer discards result of imag(a)
./defer1.go:24:2: defer discards result of len(sl)
./defer1.go:25:2: defer discards result of make([]int, 10)
./defer1.go:26:2: defer discards result of new(*int)
./defer1.go:30:2: defer discards result of real(a)

我們看到,Go 編譯器居然給出一組錯誤提示!

從這組錯誤提示中我們可以看到,append、cap、len、make、newimag 等內(nèi)置函數(shù)都是不能直接作為 deferred 函數(shù)的,而 close、copy、delete、print、recover 等內(nèi)置函數(shù)則可以直接被 defer 設(shè)置為 deferred 函數(shù)。

不過,對于那些不能直接作為 deferred 函數(shù)的內(nèi)置函數(shù),我們可以使用一個包裹它的匿名函數(shù)來間接滿足要求,以 append 為例是這樣的:

defer func() {
  _ = append(sl, 11)
}()

5.2 第二點(diǎn):注意 defer 關(guān)鍵字后面表達(dá)式的求值時機(jī)

這里,一定要牢記一點(diǎn):defer 關(guān)鍵字后面的表達(dá)式,是在將 deferred 函數(shù)注冊到 deferred 函數(shù)棧的時候進(jìn)行求值的。

我們同樣用一個典型的例子來說明一下 defer 后表達(dá)式的求值時機(jī):

func foo1() {
    for i := 0; i <= 3; i++ {
        defer fmt.Println(i)
    }
}

func foo2() {
    for i := 0; i <= 3; i++ {
        defer func(n int) {
            fmt.Println(n)
        }(i)
    }
}

func foo3() {
    for i := 0; i <= 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
    }
}

func main() {
    fmt.Println("foo1 result:")
    foo1()
    fmt.Println("\nfoo2 result:")
    foo2()
    fmt.Println("\nfoo3 result:")
    foo3()
}

這里,我們一個個分析 foo1、foo2 和 foo3 中 defer 后的表達(dá)式的求值時機(jī)。

首先是 foo1。foo1 中 defer 后面直接用的是 fmt.Println 函數(shù),每當(dāng) defer 將 fmt.Println 注冊到 deferred 函數(shù)棧的時候,都會對 Println 后面的參數(shù)進(jìn)行求值。根據(jù)上述代碼邏輯,依次壓入 deferred 函數(shù)棧的函數(shù)是:

fmt.Println(0)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)

因此,當(dāng) foo1 返回后,deferred 函數(shù)被調(diào)度執(zhí)行時,上述壓入棧的 deferred 函數(shù)將以 LIFO 次序出棧執(zhí)行,這時的輸出的結(jié)果為:

3
2
1
0

然后我們再看 foo2。foo2 中 defer 后面接的是一個帶有一個參數(shù)的匿名函數(shù)。每當(dāng) defer 將匿名函數(shù)注冊到 deferred 函數(shù)棧的時候,都會對該匿名函數(shù)的參數(shù)進(jìn)行求值。根據(jù)上述代碼邏輯,依次壓入 deferred 函數(shù)棧的函數(shù)是:

func(0)
func(1)
func(2)
func(3)

因此,當(dāng) foo2 返回后,deferred 函數(shù)被調(diào)度執(zhí)行時,上述壓入棧的 deferred 函數(shù)將以 LIFO 次序出棧執(zhí)行,因此輸出的結(jié)果為:

3
2
1
0

最后我們來看 foo3。foo3 中 defer 后面接的是一個不帶參數(shù)的匿名函數(shù)。根據(jù)上述代碼邏輯,依次壓入 deferred 函數(shù)棧的函數(shù)是:

func()
func()
func()
func()

所以,當(dāng) foo3 返回后,deferred 函數(shù)被調(diào)度執(zhí)行時,上述壓入棧的 deferred 函數(shù)將以 LIFO 次序出棧執(zhí)行。匿名函數(shù)會以閉包的方式訪問外圍函數(shù)的變量 i,并通過 Println 輸出 i 的值,此時 i 的值為 4,因此 foo3 的輸出結(jié)果為:

4
4
4
4

通過這些例子,我們可以看到,無論以何種形式將函數(shù)注冊到 defer 中,deferred 函數(shù)的參數(shù)值都是在注冊的時候進(jìn)行求值的。

5.3 第三點(diǎn):知曉 defer 帶來的性能損耗

通過前面的分析,我們可以看到,defer 讓我們進(jìn)行資源釋放(如文件描述符、鎖)的過程變得優(yōu)雅很多,也不易出錯。但在性能敏感的應(yīng)用中,defer 帶來的性能負(fù)擔(dān)也是我們必須要知曉和權(quán)衡的問題。

這里,我們用一個性能基準(zhǔn)測試(Benchmark),直觀地看看 defer 究竟會帶來多少性能損耗?;?Go 工具鏈,我們可以很方便地為 Go 源碼寫一個性能基準(zhǔn)測試,只需將代碼放在以“_test.go”為后綴的源文件中,然后利用 testing 包提供的“框架”就可以了,我們看下面代碼:

// defer_test.go
package main
  
import "testing"

func sum(max int) int {
    total := 0
    for i := 0; i < max; i++ {
        total += i
    }

    return total
}

func fooWithDefer() {
    defer func() {
        sum(10)
    }()
}
func fooWithoutDefer() {
    sum(10)
}

func BenchmarkFooWithDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fooWithDefer()
    }
}
func BenchmarkFooWithoutDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fooWithoutDefer()
    }
}

這個基準(zhǔn)測試包含了兩個測試用例,分別是 BenchmarkFooWithDefer 和 BenchmarkFooWithoutDefer。前者測量的是帶有 defer 的函數(shù)執(zhí)行的性能,后者測量的是不帶有 defer 的函數(shù)的執(zhí)行的性能。

在 Go 1.13 前的版本中,defer 帶來的開銷還是很大的。我們先用 Go 1.12.7 版本來運(yùn)行一下上述基準(zhǔn)測試,我們會得到如下結(jié)果:

$go test -bench . defer_test.go
goos: darwin
goarch: amd64
BenchmarkFooWithDefer-8        30000000          42.6 ns/op
BenchmarkFooWithoutDefer-8     300000000           5.44 ns/op
PASS
ok    command-line-arguments  3.511s

從這個基準(zhǔn)測試結(jié)果中,我們可以清晰地看到:使用 defer 的函數(shù)的執(zhí)行時間是沒有使用 defer 函數(shù)的 8 倍左右。

如果我們要用好 defer,前提就是要了解 defer 的運(yùn)作機(jī)制,這里你要把握住兩點(diǎn):

  • 函數(shù)返回前,deferred 函數(shù)是按照后入先出(LIFO)的順序執(zhí)行的;
  • defer 關(guān)鍵字是在注冊函數(shù)時對函數(shù)的參數(shù)進(jìn)行求值的。

最后,在最新 Go 版本 Go1.17 中,使用 defer 帶來的開銷幾乎可以忽略不計了,你可以放心使用。

以上就是淺析Go中函數(shù)的健壯性,panic異常處理和defer機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Go函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go并發(fā)調(diào)用的超時處理的方法

    Go并發(fā)調(diào)用的超時處理的方法

    這篇文章主要介紹了Go并發(fā)調(diào)用的超時處理的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-01-01
  • Golang多模塊開發(fā)的詳細(xì)過程

    Golang多模塊開發(fā)的詳細(xì)過程

    這篇文章主要給大家介紹了關(guān)于Golang多模塊開發(fā)的詳細(xì)過程,文中通過實(shí)例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2023-02-02
  • Go?Excelize?API源碼閱讀SetSheetViewOptions示例解析

    Go?Excelize?API源碼閱讀SetSheetViewOptions示例解析

    這篇文章主要為大家介紹了Go-Excelize?API源碼閱讀SetSheetViewOptions示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • Go程序性能優(yōu)化及pprof使用方法詳解

    Go程序性能優(yōu)化及pprof使用方法詳解

    這篇文章主要為大家詳細(xì)介紹了Go程序性能優(yōu)化及pprof的使用方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-11-11
  • 詳解golang 定時任務(wù)time.Sleep和time.Tick實(shí)現(xiàn)結(jié)果比較

    詳解golang 定時任務(wù)time.Sleep和time.Tick實(shí)現(xiàn)結(jié)果比較

    本文主要介紹了golang 定時任務(wù)time.Sleep和time.Tick實(shí)現(xiàn)結(jié)果比較,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-02-02
  • golang連接池檢查連接失敗時如何重試(示例代碼)

    golang連接池檢查連接失敗時如何重試(示例代碼)

    在Go中,可以通過使用database/sql包的DB類型的Ping方法來檢查數(shù)據(jù)庫連接的可用性,本文通過示例代碼,演示了如何在連接檢查失敗時進(jìn)行重試,感興趣的朋友一起看看吧
    2023-10-10
  • golang?recover函數(shù)使用中的一些坑解析

    golang?recover函數(shù)使用中的一些坑解析

    這篇文章主要為大家介紹了golang?recover函數(shù)使用中的一些坑解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-02-02
  • go?mod?tidy報錯解決方法詳解

    go?mod?tidy報錯解決方法詳解

    這篇文章主要為大家介紹了go?mod?tidy報錯解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • IdeaGo啟動報錯Failed to create JVM的問題解析

    IdeaGo啟動報錯Failed to create JVM的問題解析

    這篇文章主要介紹了IdeaGo啟動報錯Failed to create JVM的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-11-11
  • go語言操作redis連接池的方法

    go語言操作redis連接池的方法

    這篇文章主要介紹了go語言操作redis連接池的方法,涉及Go語言操作radis的技巧,需要的朋友可以參考下
    2015-03-03

最新評論