掘金 后端 ( ) • 2024-06-24 09:26

背景

这个问题的产生来源于小泉在开发rpc接口时返回error遇到的问题,开发时想在defer里对err进行最终的统一处理赋值,发现外层接收一直都未生效。问题可以简化为成下面的小demo。

func returnError() error {
    var err error
    defer func() {
       //err = errors.New("defer error")
       err = nil
    }()
    err = errors.New("test error")
    return err
}
func main() {
    fmt.Printf("return error : %v\n", returnError())
}

这个函数会输出什么呢?大家可以自己试一试。

在问题实验之前,我们先介绍一下本文可能会涉及到的一些Go的基本概念。

文章涉及代码部分已放置github。

github地址:go-demo

指针receiver

interface可以理解为方法的集合体,它是某一类对象的行为表现和Java中的interface如出一辙,而实现该interface所有方法的对象(结构体)都可以作为该interface类型,即实现该interface。以如下Box接口以及BigBox结构体为例来作为该节内容说明。

type Box interface {
    Color() string
    Color2() string
}

type BigBox struct {
    ColorStr string
    Volume   float64
}

func (b BigBox) Color() string {
    return b.ColorStr
}

func (b BigBox) Color2() string {
    return b.ColorStr
}

// 测试
func (b BigBox) SetColor(c string) BigBox {
    b.ColorStr = c
    return b
}

func (b *BigBox) SetColor2(c string) {
    b.ColorStr = c
}

func main() {
    box := BigBox{}
    boxCopy := box.SetColor("red")
    var box2 Box = box
    fmt.Printf("after SetColor return box color: %v\n", box2.Color())
    fmt.Printf("after SetColor return boxCopy color: %v\n", boxCopy.Color())
    var box3 Box = &box
    box.SetColor2("red")
    fmt.Printf("after SetColor2 return box color: %v\n", box3.Color())
}

Box接口内方法由BigBox结构体实现,同时定义了两个方法SetColorSetColor2

  • SetColorBigBox作为receiver,同时返回值为BigBox类型
  • SetColor2*BigBox即指针作为receiver。

main函数中我们定义box作为BigBox实例对象,并分别使用SetColorSetColor2ColorStr进行赋值,同时SetColor时返回赋值后的box称之为boxCopy对象,在打印值时会发现:

对于box对象来说,SetColor并没有生效,而boxCopy对象生效了。这是因为在调用非指针receiver接收的方法时Go语言会对box进行拷贝,在赋值时并非对box对象进行了赋值,因此在测试时,boxCopy对象的Color值赋值成功,而box的未成功。而指针型receiver则不会进行拷贝,而是直接赋值。

同时你可以看到对box2、box3的赋值的不同(对象,指针对象),但是都可以作为Box对象的实例

defer介绍

defer这里小泉只做些简短介绍(在学了,在学了),它主要是起到延迟调用的作用,defer关键字的写入触发方式是按照栈的方式,写入用先到后入栈,出栈则由后到先出栈。

其调用链路如下:

image.png

return先完成赋值语句,再去执行defer,最后再执行一次return返回函数调用处。

return语句如果赋值非指针类型,则会发生值拷贝。

error

接下来我们看下error类型。error类型是Go语言中最常用到的数据类型,无时无刻,随时随地,我们都要if err != nil所以我们来看下error的结构。

type error interface {
    Error() string
}

error本身只是一个接口。我们所使用的error都是结构体通过实现Error()方法从而可以作为error被使用。

再看一下我们最常见的errors.New方法底层代码,也正是由于这个方法,我们会对最初的问题产生分歧。

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

我们常用的error大多数都会通过errors.New()方法去创建error,而此时返回的error&errorString,所以很多人会认为此时拿到error类型应该是指针呀。

但根据前文interface相关的分析,这里其实做了一层转换,此时针对类型而言,函数调用返回值类型就是error类型,而不是*errorstring,即非指针类型

问题

在做了诸多前置解释之后,我们来做点小demo实验吧。

实验

func returnError() error {
    var err error
    defer func() {
       err = nil
    }()
    err = errors.New("test error")
    return err
}

func returnError2()(err error) {
    defer func() {
       err = nil
    }()
    err = errors.New("test error")
    return err
}

func returnErrorPtr() *error {
    var err error
    defer func() {
       err = nil
    }()
    err = errors.New("test error")
    return &err
}
func main() {
    fmt.Printf("return error by return: %v\n", returnError())
    fmt.Printf("return error by err return: %v\n", returnError2())
    fmt.Printf("return error by ptr: %v\n", *returnErrorPtr())
}

结果

解释

returnError return非指针类型,发生浅拷贝赋值完成,然后defer执行去修改局部变量,对return赋值的变量无影响。

returnError2 已经声明变量err了,因此returndefer函数内操作的都是都是err变量。

returnErrorPtr 指针型变量,返回值的本身就是地址,因此同样操作的都是指针地址下的内容。

总结

小泉自我感受,Go语言很多时候在变量赋值方面会帮开发去做浅拷贝操作,所以一般最好指针实例化对象(inteface、结构体类型),同时记得return赋值非指针对象(包括结构体、interface)会发生拷贝逻辑,所以对局部变量的修改都不会影响返回值的结果哦。

以及最后一点,error也不是指针类型!!!!