为什么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上的这个练习题自己试验一下。