爲什麼Go中有的自定義error會導致內存溢出

分享一個在go tour上看到的練習題,練習裏要求用戶自己定義一個錯誤類型,實現error接口,函數在參數不滿足條件的時候返回自定義的錯誤類型的值。練習中特別提示用戶不要在實現的Error方法裏直接使用fmt.Sprint(e)以避免造成程序內存溢出。

下面貼一下具體的練習題

Practice

從之前的練習中複製 Sqrt 函數,修改它使其返回 error 值。

Sqrt 接受到一個負數時,應當返回一個非 nil 的錯誤值。複數同樣也不被支持。

創建一個新的類型

type ErrNegativeSqrt float64

併爲其實現

func (e ErrNegativeSqrt) Error() string

方法使其擁有 error 值,通過 ErrNegativeSqrt(-2).Error() 調用該方法應返回 "cannot Sqrt negative number: -2"

注意:Error 方法內調用 fmt.Sprint(e) 會讓程序陷入死循環。可以通過先轉換 e 來避免這個問題:fmt.Sprint(float64(e))。這是爲什麼呢?

修改 Sqrt 函數,使其接受一個負數時,返回 ErrNegativeSqrt 值。

Solution

這裏只爲敘述返回error的情況,所以請忽略Sqrt函數的功能實現。

package main

import (
    "fmt"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
  // 這裏直接使用e值會內存溢出
    return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}

func Sqrt(x float64) (float64, error) {
    if x < 0 {
        err := ErrNegativeSqrt(x)
        return 0, err
    }
    return 0, nil
}

func main() {
    fmt.Println(Sqrt(2))
    fmt.Println(Sqrt(-2))
}

接下來探究一下爲什麼在練習中把值e先轉換爲float64類型後程序就不會再內存溢出。

fmt.Sprint(e)將調用e.Error()e轉換爲字符串。如果Error()方法調用fmt.Sprint(e),則程序將遞歸直到內存溢出。可以通過將e轉換成一個非錯誤類型(未實現Error接口)的值來避免這種情況。

實際上在Error方法中把error值直接傳遞給fmt包中Print相關的函數都會導致無限循環。原因可以在fmt包的源碼中找到。

        switch verb {
        case 'v', 's', 'x', 'X', 'q':
            // Is it an error or Stringer?
            // The duplication in the bodies is necessary:
            // setting wasString and handled, and deferring catchPanic,
            // must happen before calling the method.
            switch v := p.field.(type) {
            case error:
                wasString = false
                handled = true
                defer p.catchPanic(p.field, verb)
                // 這裏調用了Error方法
                p.printField(v.Error(), verb, plus, false, depth)
                return

通過鏈接可以在Github上看到這塊詳細的源碼 https://github.com/golang/go/...

這個練習感覺還是給開發者提示了一個非常隱蔽的坑,感興趣的可以去go tour上的這個練習題自己試驗一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章