在Go语言中函数的返回值使用命名参数一般不常用。本小节将讨论在什么情况下使用它使得API更加方便。在讨论之前,我们先来看一下命名返回参数工作原理。
在方法或函数的返回值参数的类型前可以添加参数名,并且它们可以当做普通的变量。当命名一个返回参数时,参数变量在函数/方法的开始被初始化为零值。这样在函数的返回时候直接写return即可,不用在return后面跟上返回值,函数的返回值就是返回参数类型前的变量内容。
func f(a int) (b int) {
b = a
return
}
如上,函数f的返回值是命名参数b,当函数返回的时候直接return即可,返回的内容就是b的值。
那命名函返回参数在什么情况下使用呢?
下面的接口包含一个getCoordinates方法,该方法根据输入的地址返回坐标信息
type locator interface {
getCoordinates(address string) (float32, float32, error)
}
因为上述接口是一个内部接口(locator不可导出),所以说明文档不是必须的。当你读到这里的代码时候,你能猜测各个float32表示的含义吗?或许你猜到是经度、纬度值,但是具体哪个是经度哪个是纬度呢?根据惯例,纬度并不总是第一个参数,所以不得不检查具体的实现。这种情况下,推荐使用命名函数返回参数让代码根据可读性, 代码如下。
type locator interface {
getCoordinates(address string) (lat, lng float32, err error)
}
上面的是新版本代码,采用有命名的函数返回参数,可以清晰地看到第一个参数表示纬度,第二是经度。
下面讨论什么时候在方法实现中使用命名返回参数的场景。是否应该将命名返回参数作为实现本身的一部分?
func (l loc) getCoordinates(address string) (
lat, lng float32, err error) {
// ...
}
在这种情况下,有名返回参数可以帮助读代码的人,我们可以使用命名的参数。
「NOTE,如果一个函数返回同一个类型的多个结果,可以考虑创建一个有具体意义字段名的结构体。然而,这种情况并不是总是可能的,例如当需要实现已有的接口时,它的签名已经固定」
下面的代码将存储customer信息到数据库中,现在来考虑另外一种函数签名。
func StoreCustomer(customer Customer) (err error) {
// ...
}
上述的返回值err对开发者来说没有什么特别的帮助。在这种情况下,我们倾向于不使用有命名的返回返回参数。
现在给出什么时候该采用有命名函数返回参数的结论,它依赖上下文环境, 在大多数情况下,如果使用有命名函数返回参数不能让代码更具有可读性,我们不应该使用它。
另一个要考虑到的是,在某些情况下,已经初始化的命名函数返回参数可以使得代码处理更方便,即使它们在可读性方面没有什么帮助。下面的代码就是这样一个例子,该代码是Effective Go书中提倡的写法。
func ReadFull(r io.Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
上述代码采用了有名函数返回参数,虽然它没有提升代码的可读性,但是,n和err一开始就初始化了,让代码变得更精简。另一方面,这个函数可能会让读者初看有些困惑,这需要找到一个平衡点。
对于使用有名函数返回参数问题,一个注意项是:在短函数中,它们是比较好接受的,否则,会可读性变差。因为读者记住整个函数的输出。同时,我们在使用时要保持一致,要么直接return不带参数,要么return全部使用带参数的返回。
总结,命名返回参数使用规则如下:
因此,在实际使用中,如果使用有名参数有明显优点时,我们采用有名函数返回参数。
在前面小节,分析了有名函数返回参数在某些场景很有用的。然而,如果我们不够小心,一开始就初始化返回变量可能会导致微妙的错误,本小节将讨论这些问题。
继续沿用前面小节中给定一个地址返回它的经度和纬度值例子说明,当返回两个float32时,我们将决定使用命名的函数参数来明确显示纬度和经度。该函数首先将验证给定的地址,然后获取坐标。在此期间,它将对输入的上下文进行检查,以确保它没有被取消或者没有超过截止日期。
下面是getCoordinates一种新的实现,这段代码有什么问题吗?
func (l loc) getCoordinates(ctx context.Context, address string) (
lat, lng float32, err error) {
isValid := l.validateAddress(address)
if !isValid {
return 0, 0, errors.New("invalid address")
}
if ctx.Err() != nil {
return 0, 0, err
}
// Get and return coordinates
}
咋一看,没有什么问题。其实是有问题的,重点是 if ctx.Err()!=nil条件的返回值是err. 然而该err却还没有被赋值,它任然是一开始初始化的零值(nil). 因此,这将会return nil。
然而上面的代码是可以编译的,因为err是有名返回参数,一开始就初始化了。如果不是有名返回参数,代码是不会通过编译的,会提示.
Unresolved reference 'err'
一个可能的修改方法是,将ctx.Err()的返回值赋值给err,代码如下:
if err := ctx.Err(); err != nil {
return 0, 0, err
}
但是上面的代码ctx.Err()函数返回值赋值给定的一个if作用域内的err,这会屏蔽掉有名返回参数中的err. 这打破了既定的规则,我们不应该将直接返回和有名参数返回混在一起使用。记住,使用有名返回参数并不一定意味着直接使用裸返回语句,可以使用有名返回参数使得签名更清晰。另一种处理方法是使用裸返回语句,代码如下
if err = ctx.Err(); err != nil {
return
}
总结,在某些情况下,例如多次返回同一类型,我们可以使用有名函数返回参数,它可以提高代码的可读性,有时候也可以让处理逻辑更简洁。但是,我们必须要记住,因为参数一开始被初始化为零值,就像本节前面举的例子,它会导致微妙的错误,这些错误在阅读代码的时候是不容易发现的。因此,在使用有名函数返回参数时,我们要格外小心,以避免潜在的副作用。