本节中讨论返回接口的影响,以及为什么在某些情况下会导致错误。这个错误可能是Go中最普遍的错误之一,因为它可能被认为是违反直觉的,至少在我们遇到它之前。
考虑下面的例子,我们定义了一个Customer结构体,并实现了Validate方法来进行安全性检查。然而,我们不想返回第一个错误,而是返回一个错误列表。所以,我们将创建一个自定义错误类型来处理这种情况。
type MultiError struct {
errs []string
}
func (m *MultiError) Add(err error) {
m.errs = append(m.errs, err.Error())
}
func (m *MultiError) Error() string {
return strings.Join(m.errs, ";")
}
MultiError实现了error接口的Error() string
方法,所以它实现了error接口,与此同时,它对外提供了Add方法添加error到MultiError中。我们可以在Customer.Validate方法中使用MultiError对象校验年龄和名称,在校验全部通过的情况下,返回一个nil error.
func (c Customer) Validate() error {
var m *MultiError
if c.Age < 0 {
m = &MultiError{}
m.Add(errors.New("age is negative"))
}
if c.Name == "" {
if m == nil {
m = &MultiError{}
}
m.Add(errors.New("name is nil"))
}
return m
}
在上面的实现中,m刚开始被初始化为*MultiError
类型的零值,为nil. 在校验失败的情况下,会分配一个MultiError对象给它,并且向它里面Add一个error。在最后将m返回,此时m的值要么是nil要么是指向MultiError对象的指针。
下面对上面的代码进行测试,待验证的Customer是一个合法的对象。
customer := Customer{Age: 33, Name: "John"}
if err := customer.Validate(); err != nil {
log.Fatalf("customer is invalid: %v", err)
}
上面代码输出结果为:
2021/05/08 13:47:28 customer is invalid: <nil>
输出结果相当奇怪,因为Customer是一个合法的对象,然而它却匹配上err != nil
条件,输出的log日志打中为nil,这是为什么呢?
在Go语言中,我们知道一个指针接收器可以是nil. 下面创建一个假类型并使用它的nil指针接收器调用方法进行验证。
type Foo struct{}
func (foo *Foo) Bar() string {
return "bar"
}
func main() {
var foo *Foo
fmt.Println(foo.Bar())
}
foo被初始化为指针的零值(nil)。然而上面的代码是可以编译通过的,并且运行会输出bar,因为nil指针是有效的接收器。
为什么会这样呢?在Go语言中,接收器是一个语法糖,可以将其理解为方法的第一个参数为接收器对象,上面的Bar方法可以理解为下面的代码。
func Bar(foo *Foo) string {
return "bar"
}
我们知道传递一个nil指针给一个函数是有效的,因此,使用nil指针作为接收器是有效的。
现在我们回到最开始的例子。
func (c Customer) Validate() error {
var m *MultiError
if c.Age < 0 {
// ...
}
if c.Name == "" {
// ...
}
return m
}
上面程序中的m被初始化为指针的零值(nil), 如果Age和Name都合法,返回值不是直接的nil而是nil指针。由于nil指针是一个有效的接收器,返回的结果不再是nil值,而是被转换为interface。换句话说,Validate的调用方法将总是会得到一个非零错误。
为了搞清楚这一点,我们需要记住在Go语言中,interface是一个wrapper. 本例中,被包装的对象是nil(MultiError对象指针), 然而包装者并不是nil,而是error接口,如下图所示。
因此,不管提供的Customer是什么,调用者在调用Validate方法之后将总是得到一个非nil的error。在Go语言中,这是一个普遍的错误,需要认真理解。
那如何修复上面例子中存在的问题呢?很简单,如果Name和Age都是合法的,直接在函数末尾返回nil而不是m, 代码如下
func (c Customer) Validate() error {
var m *MultiError
if c.Age < 0 {
// ...
}
if c.Name == "" {
// ...
}
if m != nil {
return m
}
return nil
}
上述代码在函数尾会检查m是否为nil, 如果为非nil,直接返回m, 否则直接返回nil. 因此在Customer都合法的情况下,返回的是一个nil接口,而不是一个nil接收器被转换为一个非nil的接口。
总结,在Go语言中,允许使用nil作为函数的接收器,而从nil指针转换的接口不再是nil接口。因此,当我们必须返回一个接口时,不应该直接返回一个nil指针,而应该是一个nil值。通常来说,拥有一个nil指针不是一个理想的情况,这意味着一个可能的错误。前面的代码只是一个示例,注意的是这种问题不仅仅是与错误有关,而是使用指针接收器实现的任何接口都有可能会产生上述问题。