Go 函数返回nil避坑


前言

go语言写函数时经常返回nil,然后在函数外面判断返回值是否为空。这里有个bug,记录一下

问题1

(*Type)(nil) ≠ nil

func returnsError() error {
	var p *MyError = nil
	if bad() {
		p = ErrBad
	}
	return p // Will always return a non-nil error.
}

上面函数returnsError返回的 p 永远不会与 nil 相等。

这是为什么呢,因为 error 是一个 interface,interface 之间比较需要保证两者的 Type 和 Value 两两相等

语言内的 nil 可以理解为一个 Type 和 Value 均为空的 interface
代码里面返回的 p 虽然 Value 为空,但是 Type 是 *MyError
所以 p!=nil 。

正确写法

func returnsError() error {
	if bad() {
		return ErrBad
	}
	return nil
}

这个问题不仅仅是抛出错误的时候会出现,任何返回 interface 的场景都需要注意。

问题2

type CustomError struct {
	Metadata map[string]string
	Message string
}

func (c CustomError) Error() string {
		return c.Message
}

var (
	ErrorA = CustomError{Message:"A", Matadata: map[string]string{"Reason":""}}
	ErrorB = CustomError{Message:"B"}
)

func DoSomething() error {
	return ErrorA
}

而我们在外部接收到错误之后常常会使用 errors.Is 来判断错误类型:

err:=DoSomething()
if errors.Is(err, ErrorA) {
	// handle err
}

但是会发现上面这个判断无论如何都是 false。研究一下 errors.Is 的源码:

func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflect.TypeOf(target).Comparable()
	for {
		if isComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		if err = errors.Unwrap(err); err == nil {
			return false
		}
	}
}

可以看到这是一个在 error tree 上递归的流程,真值的终结条件是 err==target ,但是前提是 target 本身得是 comparable 的

所以如果我们把一个 map 放入了 error struct,就导致这个 error 变为 incomparable,永远无法成功比较。

解决方案也很简单,就是将 Error 定义指针类型:

var (
	ErrorA = &CustomError{Message:"A", Matadata: map[string]string{"Reason":""}}
	ErrorB = &CustomError{Message:"B"}
)

指针类型比较只需要是否检查是否指向同一个对象,这样就能顺利比较了。

参考

[1]深入理解 Go Comparable Type


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录