明白了:当 nil != nil 时
在我的第一个 Go 项目中,我实现了自己的函数Error struct,以传达比 Go 内置函数error interface提供的简单错误消息更详细的错误信息。
我最初实现的时候,是用 `<div>` 来实现的struct。后来我发现用 `<div>` 更好interface:
package myerr
type Code string
type Error interface {
error // implementations must satisfy error interface
Code() Code // project-specific error code
Cause() Error // the Error that caused this Error, if any
OrigErr() error // the original error
// ...
}
关于错误代码的题外话
我之所以选择
string使用整数而不是二进制数作为错误代码,是因为整数的唯一优势在于它们便于类 C 语言代码处理switch。然而,整数代码也存在一些缺点,包括:
- 它们对人类读者来说并不友好:你必须查找代码,或者错误提供程序必须包含字符串描述(在这种情况下,提供程序一开始就把代码做成字符串还不如)。
- 您必须确保来自不同子系统的代码不会冲突。
是的,比较字符串和比较整数确实会略微降低性能,但是:
- 想必错误处理是例外情况,所以代码运行速度稍慢一些也无妨,因为它不是关键路径上的代码。
- 如果你正在开发一个 REST 应用,其中错误代码是通过 HTTP 以 JSON 响应的形式发送的,那么由于错误代码是以文本形式通过网络传输的,因此在
itoa处理和atoi解析整数代码(更不用说解析整个 JSON 文档)时,性能就已经受到了影响。所以,额外的字符串比较操作只会增加性能负担。
尽管现在是一个interface,Error但仍然需要由一个来实现struct:
type MyError struct {
code Code
origErr error
cause *MyError
}
func (e *MyError) Cause() Error {
return e.cause
}
func (e *MyError) Code() Code {
return e.code
}
func (e *MyError) OrigErr() error {
return e.origErr
}
这一切看起来都相当简单明了。(注:其他用于生成错误的功能在此省略,因为它们与本文无关;但它们同样简单明了。)
问题
当时我正在编写一些负面单元测试(确保能够正确检测和报告错误),因此我需要一种方法来比较两个Error对象。GoDeepEqual()的标准库中有一个 `isEquals` 函数,虽然它能用,但它只能告诉你两个对象是否相等。对于测试来说,如果两个对象不相等,那么知道具体不相等的是什么就很有帮助,所以我编写了ErrorDeepEqual()一个 `isEquals` 函数,它返回一个error描述此情况的 `isEquals` 对象:
func ErrorDeepEqual(e1, e2 myerror.Error) error {
if e1 == nil {
if e2 != nil {
return errors.New("e1(nil) != e2(!nil)")
}
return nil
}
if e2 == nil {
return errors.New("e1(!nil) != e2(nil)")
}
if e1.Code() != e2.Code() {
return fmt.Errorf("e1.Code(%v) != e2.Code(%v)", e1.Code(), e2.Code())
}
// ...
return ErrorDeepEqual(e1.Cause(), e2.Cause())
}
由于一个条件Error可能存在原因,ErrorDeepEqual()因此最终会通过递归调用来检查这些原因是否相等。问题在于其中一个测试nil在这一行因指针问题而引发 panic:
if e1.Code() != e2.Code() { // panics here because e1 is nil
但是if这行代码前面的几行代码检查了nil,所以这里既不是e1也不e2是nil,对吧?怎么可能e1 == nil又false是e1呢nil?
原因
事实证明,在 Go 语言中,这种情况确实会发生,而且让很多人感到困惑,以至于专门为此设立了一个常见问题解答。简单来说,一个 `int` 对象interface包含两部分:类型和该类型的值。只有当类型和值都interface为 `int` 时,`int` 对象才是`int` 对象。nilnil
var i interface{} // i = (nil,nil)
fmt.Println(i == nil) // true
var p *int // p = nil
i = p // i = (*int,nil)
fmt.Println(i == nil) // false: (*int,nil) != (nil,nil)
fmt.Println(i == (*int)(nil)) // true : (*int,nil) == (*int,nil)
fmt.Println(i.(*int) == nil) // true : nil == nil
现在,我们暂且搁置 Go 语言为何以这种方式运行(以及这种运行方式是否合理)的理由。
修复方案
与此同时,我仍在努力弄清楚为什么会得到一个nil Error带有nil值的非零值。结果发现罪魁祸首是:
func (e *MyError) Cause() Error {
return e.cause // WRONG!
}
问题在于,无论何时给一个值赋值interface(无论是显式赋值还是隐式赋值),它都会同时return具有该值及其具体类型。一旦它具有了具体类型,它就永远不会与(无类型的)相等。解决方法是:nil
func (e *MyError) Cause() Error {
if e.cause == nil { // if typed pointer is nil ...
return nil // return typeless nil explicitly
}
return e.cause // else return typed non-nil
}
也就是说:当一个函数的返回类型是interface并且你可以返回一个nil指针时,你必须检查并且如果指针是 ,nil则返回字面量。nilnil
理由
Go 语言之所以这样运行,有几个原因。go -nuts 邮件列表上有一个很长的讨论帖。我仔细阅读了帖子,并总结出了主要原因。
首先,Go 允许任何用户自定义类型为其定义方法,而不仅仅是struct其他语言中的类型(或类)。例如,我们可以定义自己的类型int并Stringer interface为其实现方法:
type Aint int // accounting int: print negatives in ()
func (n Aint) String() string {
if n < 0 {
return fmt.Sprintf("(%d)", -n)
}
return fmt.Sprintf("%d", n)
}
现在,让我们使用一个Stringer interface变量:
var n Aint // n = (Aint,0)
var s fmt.Stringer = n // s = (Aint,0)
fmt.Println(s == nil) // false: (Aint,0) != (nil,nil)
这里,s是非的,nil因为它指的是一个Aint——而它的值恰好是0。0值没有什么特别之处,因为0对于一个来说是一个非常好的值int。
第二个原因是 Go 语言具有非常强的类型。例如:
fmt.Println(s == 0) // error: can't compare (Aint,0) to (int,0)
这是一个编译时错误,因为你不能将s(指的是一个Aint)与 0 进行比较(因为普通的 0 是类型为 的int)。但是,你可以使用类型转换或类型断言:
fmt.Println(s == Aint(0)) // true: (Aint,0) == (Aint,0)
fmt.Println(s.(Aint) == 0) // true: 0 == 0
第三个原因是Go 明确允许nil方法具有指针接收器:
type T struct {
// ...
}
func (t *T) String() string {
if t == nil {
return "<nil>"
}
// ...
}
就像 0 对于 `int` 来说是一个完全合适的值一样Aint,nil对于指针来说也是一个完全合适的值。由于 ` nilint` 不指向任何 `int` T,因此它可以用来实现 `int` 的默认行为T。重复之前的例子,Aint但这次是 `int` *T:
var p *T // p = (*T,nil)
var s fmt.Stringer = p // s = (*T,nil)
fmt.Println(s == nil) // false: (*T,nil) != (nil,nil)
fmt.Println(s == (*T)(nil)) // true : (*T,nil) == (*T,nil)
fmt.Println(s.(*T) == nil) // true : nil == nil
我们对指针也得到了类似的结果——几乎如此。(你能看出其中的矛盾之处吗?)
哪里出了问题
与之前将类型为 的元素与类型为 0 的元素Aint进行比较会出错的例子不同:sAintint
fmt.Println(s == 0) // error: can't compare (Aint,0) to (int,0)
而将s(类型为*T)与nil(类型为nil)进行比较是可以的:
fmt.Println(s == nil) // OK to compare
问题在于,在 Go 语言中,nil根据上下文的不同,它有两种不同的含义:
nil指的是值为 0 的指针。nil指interface类型为空且值为 0 的对象。
假设 Go 语言nilinterface为情况 2nil准备了另一个关键字,而该关键字则专门用于情况 1。那么上面的代码行应该写成:
fmt.Println(s == nilinterface) // false: (*T,nil) != nilinterface
这样一来,就很明显你是在检查它interface本身是否为无类型且值为 0,而不是检查它的值interface。
此外,使用原始值nil会导致错误(原因与你不能Aint在不进行类型转换或类型断言的情况下将一个值与 0 进行比较相同):
fmt.Println(s == nil) // what-if error: can't compare (*T,nil) to (nil,nil)
fmt.Println(s == (*T)(nil)) // still true: (*T,nil) == (*T,nil)
fmt.Println(s.(*T) == nil) // still true: nil == nil
Go语言中令人困惑的地方在于,当新手程序员编写代码时:
fmt.Println(s == nil) // checks interface for empty, not the value
他们以为自己在检查接口的值,但实际上是在检查接口本身是否为空类型且值为 0。如果 Go 有一个类似 `is` 的独立关键字nilinterface,所有这些困惑都会消失。
黑客
如果你觉得这些都很麻烦,你可以编写一个函数来检查interface一个nil值,而忽略它的类型(如果有的话):
func isNilValue(i interface{}) bool {
return i == nil || reflect.ValueOf(i).IsNil()
}
虽然这种方法可行,但速度很慢,因为反射本身速度就慢。一种速度快得多的实现方式是:
func isNilValue(i interface{}) bool {
return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0
}
这会直接检查值部分interface是否为零,而忽略类型部分。