我已经实现了一个自定义错误类型,并且在 nil 值方面确实得到了奇怪的行为。当我确实将自定义错误作为标准错误接口传递时,即使自定义错误返回为 nil,这也永远不会被识别为 nil。
看看这个小测试程序:
package main
import (
"fmt"
"strconv"
)
type CustomError struct {
Code int
}
func (e *CustomError) Error() string {
return strconv.Itoa(e.Code)
}
func FailCustom(dofail bool) *CustomError {
if dofail {
return &CustomError{Code: 42}
} else {
return nil
}
}
func WrapFailCustom(dofail bool) error {
return FailCustom(dofail)
}
func main() {
err := WrapFailCustom(false)
if err == nil {
fmt.Println("err is nil")
} else {
fmt.Println("err is not nil")
}
}
操场上相同:https://play.golang.org/p/7bqeDw5B5fU
这实际上确实输出"err 不是零"。
我本来希望 *CustomError 类型的 nil 值被隐式转换为类型错误的 nil 值。谁能向我解释一下,为什么不是这种情况以及如何正确传播自定义错误类型的 nil 值?
编辑:正如伊恩·邓肯(Iain Duncan)指出的那样,可以在此处找到对此的解释
为了进一步探讨这个问题,让我们考虑对 WrapFailCustom 进行以下修改:
func WrapFailCustom(dofail bool) error {
err := FailCustom(dofail)
if err == nil {
return nil
} else {
return err
}
}
这实际上确实返回"err is nil":https://play.golang.org/p/mEKJFyk5zqf
依靠它作为解决方案,我确实感觉非常糟糕,因为在使用确实吐出我的自定义错误的函数时很容易忘记它。是否有更好的方法来制作自定义错误,以防止这种"歧义"发生?一直只使用基本错误类型的明显解决方案,对于使用 WrapFailCustom 等函数的代码来说似乎真的很不方便,所以我想避免这种情况......
有关背景信息,请参阅隐藏 nil 值,了解 golang 在此处失败的原因;和 Go 常见问题解答:为什么我的 nil 错误值不等于 nil?
Go 要求您明确类型和转换(例如,您不能将int32
类型的值添加到int
类型的值),但接口转换和自动接口值创建是此规则的例外。每当需要接口类型的值时,您可以使用其类型实现(满足)接口类型的任何值,并且将自动为您创建接口值。
您的职能:
func WrapFailCustom(dofail bool) error {
return FailCustom(dofail)
}
WrapFailCustom()
有一个返回类型error
,你尝试返回FailCustom()
函数的结果,其返回类型是*CustomError
这与error
不同!这应该引起危险信号!
什么/如何退货?将自动创建error
类型的接口值!*CustomError
实现了error
,所以一切都很好,但正如你所经历的,如果指针nil
,这种隐式值包装不会导致接口值被nil
,而是一个非nil
接口值包装nil
和类型*CustomError
。
解决方案是什么?
照顾根本原因
真的有道理吗/FailCustom()
拥有error
以外的返回类型是否有任何价值?如果没有,最简单的方法是在"问题"的来源处处理它:
func FailCustom(dofail bool) error {
if dofail {
return &CustomError{Code: 42}
}
return nil
}
然后你所有的问题都消失了。如果您按照"Go 方式"使用error
类型返回错误,这完全足够且令人满意。您甚至不再需要WrapFailCustom()
功能。
在WrapFailCustom()
中手动核算它
如果您确实需要FailCustom()
返回自定义*CustomError
类型,则需要在WrapFailCustom()
中"手动"考虑它。我会这样写:
func WrapFailCustom(dofail bool) error {
if customErr := FailCustom(dofail); customErr != nil {
return customErr
}
return nil
}
(请注意,我故意使用了不同的customErr
名称而不是err
,表示它不是error
类型,并且应该注意如何将其转换为error
。
使用作为接口类型的自定义error
类型
如果您想/需要使用自定义错误类型,另一种好方法是创建一个描述其包含的"额外"功能的接口类型:
type CustomErr interface {
Error // Embed error interface
Code() int
}
然后我们还需要实现这个Code()
方法:
func (e *CustomError) Code() int { return e.Code }
这有什么用?
根本原因将通过返回此接口类型的值(而不是指针)来解决:
func FailCustom(dofail bool) CustomErr {
if dofail {
return &CustomError{Code: 42}
}
return nil
}
隐式接口值将在FailCustom()
中创建。
此外,WrapFailCustom()
变得不必要/无用。FailCustom()
返回一个既是error
的值,您可以使用其Code()
方法从中获取Code
。返回值是一个接口值,它是一个error
,你可以在需要error
值的地方使用它。混凝土类型CustomError
甚至可以不出口(隐藏)。
与这种方法相关,请查看Dave Cheney:不要只是检查错误,要优雅地处理它们,尤其是标题为:断言行为错误而不是类型的部分。