前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#45 Returning a nil receiver

Go语言中常见100问题-#45 Returning a nil receiver

作者头像
数据小冰
发布2022-08-15 15:13:40
6120
发布2022-08-15 15:13:40
举报
文章被收录于专栏:数据小冰
返回零接收器

本节中讨论返回接口的影响,以及为什么在某些情况下会导致错误。这个错误可能是Go中最普遍的错误之一,因为它可能被认为是违反直觉的,至少在我们遇到它之前。

考虑下面的例子,我们定义了一个Customer结构体,并实现了Validate方法来进行安全性检查。然而,我们不想返回第一个错误,而是返回一个错误列表。所以,我们将创建一个自定义错误类型来处理这种情况。

代码语言:javascript
复制
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.

代码语言:javascript
复制
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是一个合法的对象。

代码语言:javascript
复制
customer := Customer{Age: 33, Name: "John"}
if err := customer.Validate(); err != nil {
        log.Fatalf("customer is invalid: %v", err)
}

上面代码输出结果为:

代码语言:javascript
复制
2021/05/08 13:47:28 customer is invalid: <nil>

输出结果相当奇怪,因为Customer是一个合法的对象,然而它却匹配上err != nil条件,输出的log日志打中为nil,这是为什么呢?

在Go语言中,我们知道一个指针接收器可以是nil. 下面创建一个假类型并使用它的nil指针接收器调用方法进行验证。

代码语言:javascript
复制
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方法可以理解为下面的代码。

代码语言:javascript
复制
func Bar(foo *Foo) string {
        return "bar"
}

我们知道传递一个nil指针给一个函数是有效的,因此,使用nil指针作为接收器是有效的。

现在我们回到最开始的例子。

代码语言:javascript
复制
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, 代码如下

代码语言:javascript
复制
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指针不是一个理想的情况,这意味着一个可能的错误。前面的代码只是一个示例,注意的是这种问题不仅仅是与错误有关,而是使用指针接收器实现的任何接口都有可能会产生上述问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 返回零接收器
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档