Go 語言一次真實的錯誤吞并的教訓(xùn)
在幾天前寫的代碼中,犯了幾個比較典型的錯誤,帶來不小的麻煩。特在此復(fù)現(xiàn)一下,吸取教訓(xùn)。
情景描述
代碼中需要實現(xiàn)一個客戶端與服務(wù)器的數(shù)據(jù)重傳機制,通過write寫數(shù)據(jù)給服務(wù)器,read讀取服務(wù)器返回。一旦中途發(fā)生錯誤,每隔1s就嘗試重新寫讀數(shù)據(jù)。當(dāng)超過上下文時間,重傳失敗。重傳實現(xiàn)代碼retry如下。
- func retry(ctx context.Context) (data string, err error) {
- LOOP:
- for i:=1;;i++{
- err = write()
- if err == nil{
- res, err := read()
- if err == nil{
- data = string(res)
- return data, err
- }
- }
- log.Printf("change data failed, err: %v, retry times : %d\n", err, i)
- select {
- case <-ctx.Done():
- log.Printf("retry failed")
- break LOOP
- case <-time.After(1 * time.Second):
- }
- }
- return "", err
- }
讀寫服務(wù)器數(shù)據(jù)函數(shù)和調(diào)用重傳代碼mock如下。
- func write() error {
- return nil
- }
- func read() ([]byte, error) {
- return []byte("hello world"), errors.New("this is a error")
- }
- func main() {
- ctx,_ := context.WithTimeout(context.Background(),5*time.Second)
- _, _ = retry(ctx)
- time.Sleep(10*time.Second)
- }
write返回err為nil,read有非nil返回。這種情況下,日志輸出如下。
- 2020/07/05 09:30:57 change data failed, err: <nil>, retry times : 1
- 2020/07/05 09:30:58 change data failed, err: <nil>, retry times : 2
- 2020/07/05 09:30:59 change data failed, err: <nil>, retry times : 3
- 2020/07/05 09:31:00 change data failed, err: <nil>, retry times : 4
- 2020/07/05 09:31:01 change data failed, err: <nil>, retry times : 5
- 2020/07/05 09:31:02 retry failed
原因分析
可以看到的是,如預(yù)想的一樣:當(dāng)發(fā)生錯誤時,就重新嘗試write和read。即重傳機制生效。但是,日志中為何err會為nil,read方法的錯誤返回被吞掉了?
經(jīng)過排查,發(fā)現(xiàn)原因就在于——Go語法糖:=(短變量聲明)的不當(dāng)使用。
- err = write()
- if err == nil{
- res, err := read()
- if err == nil{
- data = string(res)
- return data, err
- }
- }
- log.Printf("change data failed, err: %v, retry times : %d\n", err, i)
在retry中,err是已被聲明的變量類型error。由于read返回的是兩個變量,故小菜刀在此利用短變量聲明res變量,接受read的第一個返回參數(shù)。但是,此舉會改變err的作用范圍:err成為了一個局部變量。什么意思呢?即此時的err被短變量聲明所作用,成為了新聲明對象,它只能作用于內(nèi)部區(qū)域了。對于外部log.Printf而言,其引用到的err還是write方法生成的err對象。因此,即使read方法返回的err不為空,log.Printf打印的還是write方法的err結(jié)果,導(dǎo)致read的err內(nèi)容被吞。
因此,為了避免此類錯誤發(fā)生,相應(yīng)代碼調(diào)整如下。
- var res []byte
- res, err = read()
- if err == nil{
- data = string(res)
- return data, err
- }
此時,當(dāng)read返回err非nil時,日志打印如下。
- 2020/07/05 09:46:16 change data failed, err: this is a error, retry times : 1
- 2020/07/05 09:46:17 change data failed, err: this is a error, retry times : 2
- 2020/07/05 09:46:18 change data failed, err: this is a error, retry times : 3
- 2020/07/05 09:46:19 change data failed, err: this is a error, retry times : 4
- 2020/07/05 09:46:20 change data failed, err: this is a error, retry times : 5
- 2020/07/05 09:46:21 retry failed
總結(jié)
一、Go語法糖——短變量聲明(:=)使用注意事項。
- :=表示聲明+賦值。
- 短變量聲明不需要聲明所有在左邊的變量。如果一些變量在同一個詞法塊中聲明,那么對于這些變量,短聲明的行為等同于賦值(同時更改了這些變量的作用域)。
二、異常判斷規(guī)則
在上述場景代碼中,是一個多層級條件判斷的情形,其判斷規(guī)則是err為nil。但這是一種不恰當(dāng)?shù)奶幚磉壿嫛:侠淼呐袛鄺l件,是對異常情況作判斷,而將正常邏輯置于條件之外。那么,修改后的retry條件判斷邏輯應(yīng)該如下所示。
- func retry(ctx context.Context) (data string, err error) {
- LOOP:
- for i:=1;;i++{
- err = write()
- if err != nil{
- log.Printf("write data failed, err: %v, retry times : %d\n", err, i)
- select {
- case <-ctx.Done():
- log.Printf("retry failed")
- break LOOP
- case <-time.After(1 * time.Second):
- }
- continue
- }
- res, err := read()
- if err != nil{
- log.Printf("read data failed, err: %v, retry times : %d\n", err, i)
- select {
- case <-ctx.Done():
- log.Printf("retry failed")
- break LOOP
- case <-time.After(1 * time.Second):
- }
- continue
- }
- data = string(res)
- return data, err
- }
- return "", err
- }
這樣,正常的處理流程,其主邏輯均在最外層,只有異常情況(err!=nil)才進入異常處理邏輯。當(dāng)采用這種判斷規(guī)則之后,就不存在多層條件嵌套語句,由語法糖帶來的問題,也不復(fù)存在。