背景
这个问题的产生来源于小泉在开发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
结构体实现,同时定义了两个方法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 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
了,因此return
、defer
函数内操作的都是都是err
变量。
returnErrorPtr
指针型变量,返回值的本身就是地址,因此同样操作的都是指针地址下的内容。
总结
小泉自我感受,Go语言很多时候在变量赋值方面会帮开发去做浅拷贝操作,所以一般最好指针实例化对象(inteface
、结构体类型),同时记得return
赋值非指针对象(包括结构体、interface
)会发生拷贝逻辑,所以对局部变量的修改都不会影响返回值的结果哦。
以及最后一点,error
也不是指针类型!!!!