背景
这个问题的产生来源于小泉在开发rpc接口时返回error
遇到的问题,开发时想在defer
里对err
进行最终的统一处理赋值,发现外层接收一直都未生效。问题可以简化为成下面的小demo。
func returnError() error {var err errordefer 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() stringColor2() string
}type BigBox struct {ColorStr stringVolume 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 = creturn b
}func (b *BigBox) SetColor2(c string) {b.ColorStr = c
}func main() {box := BigBox{}boxCopy := box.SetColor("red")var box2 Box = boxfmt.Printf("after SetColor return box color: %v\n", box2.Color())fmt.Printf("after SetColor return boxCopy color: %v\n", boxCopy.Color())var box3 Box = &boxbox.SetColor2("red")fmt.Printf("after SetColor2 return box color: %v\n", box3.Color())
}
Box
接口内方法由BigBox
结构体实现,同时定义了两个方法SetColor
、SetColor2
。
SetColor
由BigBox
作为receiver,同时返回值为BigBox
类型SetColor2
由*BigBox
即指针作为receiver。
在main
函数中我们定义box
作为BigBox
实例对象,并分别使用SetColor
、SetColor2
对ColorStr
进行赋值,同时SetColor
时返回赋值后的box
称之为boxCopy
对象,在打印值时会发现:
对于box
对象来说,SetColor
并没有生效,而boxCopy
对象生效了。这是因为在调用非指针receiver
接收的方法时Go语言会对box
进行拷贝,在赋值时并非对box
对象进行了赋值,因此在测试时,boxCopy
对象的Color
值赋值成功,而box
的未成功。而指针型receiver
则不会进行拷贝,而是直接赋值。
同时你可以看到对box2、box3的赋值的不同(对象,指针对象),但是都可以作为Box
对象的实例。
defer介绍
defer
这里小泉只做些简短介绍(在学了,在学了),它主要是起到延迟调用的作用,defer
关键字的写入触发方式是按照栈的方式,写入用先到后入栈,出栈则由后到先出栈。
其调用链路如下:
即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 errordefer 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 errordefer 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
了,因此return
、defer
函数内操作的都是都是err
变量。
returnErrorPtr
指针型变量,返回值的本身就是地址,因此同样操作的都是指针地址下的内容。
总结
小泉自我感受,Go语言很多时候在变量赋值方面会帮开发去做浅拷贝操作,所以一般最好指针实例化对象(inteface
、结构体类型),同时记得return
赋值非指针对象(包括结构体、interface
)会发生拷贝逻辑,所以对局部变量的修改都不会影响返回值的结果哦。
以及最后一点,error
也不是指针类型!!!!