命名返回变量在Go语言中的预期用途是什么?

huangapple go评论90阅读模式
英文:

What is the intended usage of named return variables in Go?

问题

我发现Go语言中提供的具名返回变量功能非常有用,因为它可以避免单独声明变量。然而,在某些情况下,我希望返回一个与函数声明中声明的返回变量不同的变量。这似乎可以正常工作,但我觉得声明一个返回变量然后返回其他内容有点奇怪。

在编写一个帮助学习Go语言的测试程序时(不是下面的程序),我发现在返回多个变量的函数的返回语句中指定返回变量有点烦人。特别是因为这些变量在函数声明中已经被命名了。现在我发现,在发布这个问题时,似乎在有命名返回变量的情况下,它们不需要在返回语句中使用,只需使用"return"关键字即可,它会隐式地使用这些命名变量。我觉得这是一个很棒的功能。

所以,虽然我可能已经在一定程度上回答了自己的问题,**有人能否告诉我下面的用法是否可行?**我相信这个应该有文档记录,但我还没有找到,而且我购买的参考书中似乎没有提到这个特性。

示例程序:

package main

func main() {
    var sVar1, sVar2 string
    println("Test Function return-values")
    sVar1, sVar2 = fGetVal(1)
    println("This was returned for '1' : " + sVar1 + ", " + sVar2)
    sVar1, sVar2 = fGetVal(2)
    println("This was returned for '2' : " + sVar1 + ", " + sVar2)
}

func fGetVal(iSeln int) (sReturn1 string, sReturn2 string) {
    sReturn1 = "This is 'sReturn1'"
    sReturn2 = "This is 'sReturn2'"

    switch iSeln {
        case 1  :  return
        default : return "This is not 'sReturn1'", "This is not 'sReturn2'"
    }
}
英文:

I find the provision of named returned variables in Go to be a useful feature, because it can avoid the separate declaration of a variable or variables. However, in some instances I want to return a different variable to the variable declared in the function as the return-variable. That appears to work ok, however I do find it a little strange to declare a return variable and then return something else.

While writing a test program to help learn Go (not the one below), I found it a little annoying specifying the return variable in the return statement of functions returning multiple variables. Particularly so, because the variables had been named in the function declaration. I now find while posting this, that it appears that where there are named return variables, they don't need to be used in the return statement, just "return" will suffice and will implicitly use the named variables. That I find this a great feature.

So, although I have possibly partly answered my own question, could someone advise if my usage below is acceptable? I'm sure this is documented, but I haven't come across it, and it doesn't appear to be in the reference-book that I purchased which I think overlooks this feature.

Basically, the rule appears to be (as far as I can determine), that where named return variables are used, that the function statement declares the variables, and also the function can optionally implicitly uses them as the return values, however this can be overridden by using explicit return values.

Example Program :

package main

func main() {
    var sVar1, sVar2 string
    println("Test Function return-values")
    sVar1, sVar2 = fGetVal(1)
    println("This was returned for '1' : " + sVar1 + ", " + sVar2)
    sVar1, sVar2 = fGetVal(2)
    println("This was returned for '2' : " + sVar1 + ", " + sVar2)
}

func fGetVal(iSeln int) (sReturn1 string, sReturn2 string) {
    sReturn1 = "This is 'sReturn1'"
    sReturn2 = "This is 'sReturn2'"

    switch iSeln {
        case 1  :  return
        default : return "This is not 'sReturn1'", "This is not 'sReturn2'"
    }
}

答案1

得分: 23

你的用法完全正确,在Go源代码中可以找到很多类似的例子。

我将尝试解释一下在Go中return语句的实际工作原理,以便更深入地理解为什么使用命名返回变量是很自然的。思考一下Go如何实现参数传递和函数返回。一旦你理解了这一点,你就会明白为什么命名返回变量是如此自然。

在Go中,所有函数的参数和返回值都通过栈传递。这与通常在C中将一些参数传递到寄存器中的方式不同。当调用一个函数时,调用者在栈上为参数和返回值分配空间,然后调用函数。

具体来说,当调用具有3个输入参数a、b、c和两个返回值的函数时:

func f(a int, b int, c int) (int, int)

栈的结构如下(低内存地址在顶部):

* a
* b
* c
* 用于返回参数1的空间
* 用于返回参数2的空间

现在很明显,给返回参数命名只是给栈上的那些位置命名。

func f(a int, b int, c int) (x int, y int)

* a
* b
* c
* x
* y

现在应该很明显了,空的return语句的作用是将x和y的值返回给调用者。

现在来看一些反汇编代码!使用go build -gcflags -S test.go编译这段代码:

package a

func f(a int, b int, c int) (int, int) {
    return a, 0
}

func g(a int, b int, c int) (x int, y int) {
    x = a
    return
}

得到的结果是:

--- prog list "f" ---
0000 (test.go:3) TEXT    f+0(SB),$0-40
0001 (test.go:3) LOCALS  ,$0
0002 (test.go:3) TYPE    a+0(FP){int},$8
0003 (test.go:3) TYPE    b+8(FP){int},$8
0004 (test.go:3) TYPE    c+16(FP){int},$8
0005 (test.go:3) TYPE    ~anon3+24(FP){int},$8
0006 (test.go:3) TYPE    ~anon4+32(FP){int},$8
0007 (test.go:4) MOVQ    a+0(FP),BX
0008 (test.go:4) MOVQ    BX,~anon3+24(FP)
0009 (test.go:4) MOVQ    $0,~anon4+32(FP)
0010 (test.go:4) RET     ,

--- prog list "g" ---
0011 (test.go:7) TEXT    g+0(SB),$0-40
0012 (test.go:7) LOCALS  ,$0
0013 (test.go:7) TYPE    a+0(FP){int},$8
0014 (test.go:7) TYPE    b+8(FP){int},$8
0015 (test.go:7) TYPE    c+16(FP){int},$8
0016 (test.go:7) TYPE    x+24(FP){int},$8
0017 (test.go:7) TYPE    y+32(FP){int},$8
0018 (test.go:7) MOVQ    $0,y+32(FP)
0019 (test.go:8) MOVQ    a+0(FP),BX
0020 (test.go:8) MOVQ    BX,x+24(FP)
0021 (test.go:9) RET     ,

这两个函数的汇编代码几乎相同。你可以清楚地看到g中栈上的a、b、c、x、y的声明,而在f中,返回值是匿名的anon3anon4

英文:

Your usage is absolutely fine and you'll find plenty of similar examples in the Go source code.

I'll attempt to explain how the return statement actually works in Go to give a deeper appreciation of why. It is useful to have a think about how Go implements parameter passing and return from functions. Once you understand that you'll understand why named return variables are so natural.

All arguments to functions and all return values from functions are passed on the stack in Go. This varies from C which usually passes some parameters in registers. When a function is called in Go the caller makes space on the stack for both the arguments and the return values then calls the function.

Specifically, when this function is called, which has 3 input parameters a, b, c and two return values

func f(a int, b int, c int) (int, int)

The stack will look like this (low memory address at the top)

* a
* b
* c
* space for return parameter 1
* space for return parameter 2

Now it is obvious that naming your return parameter just names those locations on the stack.

func f(a int, b int, c int) (x int, y int)

* a
* b
* c
* x
* y

It should now also be obvious what an empty return statement does - it just returns to the caller with whatever the values of x and y are.

Now for some disassembly! Compiling this with go build -gcflags -S test.go

package a

func f(a int, b int, c int) (int, int) {
    return a, 0
}

func g(a int, b int, c int) (x int, y int) {
    x = a
    return
}

Gives

--- prog list "f" ---
0000 (test.go:3) TEXT    f+0(SB),$0-40
0001 (test.go:3) LOCALS  ,$0
0002 (test.go:3) TYPE    a+0(FP){int},$8
0003 (test.go:3) TYPE    b+8(FP){int},$8
0004 (test.go:3) TYPE    c+16(FP){int},$8
0005 (test.go:3) TYPE    ~anon3+24(FP){int},$8
0006 (test.go:3) TYPE    ~anon4+32(FP){int},$8
0007 (test.go:4) MOVQ    a+0(FP),BX
0008 (test.go:4) MOVQ    BX,~anon3+24(FP)
0009 (test.go:4) MOVQ    $0,~anon4+32(FP)
0010 (test.go:4) RET     ,

--- prog list "g" ---
0011 (test.go:7) TEXT    g+0(SB),$0-40
0012 (test.go:7) LOCALS  ,$0
0013 (test.go:7) TYPE    a+0(FP){int},$8
0014 (test.go:7) TYPE    b+8(FP){int},$8
0015 (test.go:7) TYPE    c+16(FP){int},$8
0016 (test.go:7) TYPE    x+24(FP){int},$8
0017 (test.go:7) TYPE    y+32(FP){int},$8
0018 (test.go:7) MOVQ    $0,y+32(FP)
0019 (test.go:8) MOVQ    a+0(FP),BX
0020 (test.go:8) MOVQ    BX,x+24(FP)
0021 (test.go:9) RET     ,

Both functions assemble to pretty much the same code. You can see quite clearly the declarations of a,b,c,x,y on the stack in g, though in f, the return values are anonymous anon3 and anon4.

答案2

得分: 8

注意:CL 20024(2016年3月,适用于Go 1.7)澄清了命名返回值的用法,并在go自身的代码库中说明了何时适合使用它:

> ## all:当无用时删除公共命名返回值
>
> 命名返回值只应在公共函数和方法中使用,当它对文档有贡献时
>
> 如果命名返回值只是为了在函数体内节省几行代码,尤其是如果这意味着文档中有重复的内容,或者只是为了让程序员可以使用裸返回语句,那么就不应该使用命名返回值。(除非在非常小的函数中,不应使用裸返回)
>
> 此更改是对公共函数签名的手动审核和清理。
>
> 如果满足以下条件,则不更改签名:
>
> * 函数是私有的(不会出现在公共godoc中)
> * 文档引用了它


例如,archive/zip/reader.go#Open()使用了

func (f *File) Open() (rc io.ReadCloser, err error) {

现在使用:

func (f *File) Open() (io.ReadCloser, error) {

它的命名返回值对其文档没有任何增加,文档如下:

// Open返回一个提供对文件内容访问的`ReadCloser`。

// 可以同时读取多个文件。

正如Marcel Besixdouze评论中指出的那样

> 请注意,即使在该CL中,它也仅适用于对文档产生负面影响的用法,例如“(string string, err error)”。
>
> 当我阅读这个CL时,它似乎更多地关注于为公共方法优先提供清晰的文档,而不是关于使用该功能本身的任何问题。
>
> Rob之前建议Go的以下样式指南:“你可以使用整个语言。”
通过查看Git Blame,您可以看到由主要贡献者(Russ Cox,Andrew Gerrand等)编写的大部分函数声明选择使用命名返回值。

英文:

Note: CL 20024 (March 2016, for Go 1.7) clarifies the usage of named return values and illustrates within the code base of go itself when its usage is appropriate:

> ## all: remove public named return values when useless
>
> Named returned values should only be used on public funcs and methods
when it contributes to the documentation
.
>
> Named return values should not be used if they're only saving the
programmer a few lines of code inside the body of the function,
especially if that means there's stutter in the documentation or it
was only there so the programmer could use a naked return
statement. (Naked returns should not be used except in very small
functions)
>
> This change is a manual audit & cleanup of public func signatures.
>
> Signatures were not changed if:
>
> * the func was private (wouldn't be in public godoc)
> * the documentation referenced it


For instance, archive/zip/reader.go#Open() used

func (f *File) Open() (rc io.ReadCloser, err error) {

It now uses:

func (f *File) Open() (io.ReadCloser, error) {

Its named return values didn't add anything to its documentation, which was:

// Open returns a `ReadCloser` that provides access to the File's contents.

// Multiple files may be read concurrently.

As noted by Marcel Besixdouze in the comments

> Note that even in that CL, it only applied to usages that negatively impacted documentation, i.e. stuff like "(string string, err error)".
>
> When I read this CL it seems much more about prioritizing clean documentation for public methods than about any problem per se with using the feature itself.
>
> Rob has previously suggested the following style guide for Go: "You can use the whole language."
Looking at the Git Blame, you can see a huge percentage of function declarations written by main contributors (Russ Cox, Andrew Gerrand etc) choose to use named returns.

答案3

得分: 0

是的,这是完全可以接受的。我通常使用命名返回变量来确保在延迟错误处理中返回默认值,以确保最小可行返回,就像下面的示例一样:

//执行一对一的反射+缓存操作
func (cacheSpot CacheSpot) callOneToOne(originalIns []reflect.Value) (returnValue []reflect.Value) {

defer func() { //确保不会发生panic
    if r := recover(); r != nil {
        log.Error("正在恢复!尝试恢复缓存值时出错!%v", r)

        //调用原始函数
        returnValue = reflect.ValueOf(cacheSpot.OriginalFunc).Call(originalIns)
    }
}()

//... 进行非常棘手的反射操作,尝试缓存结果。非常容易出错。可能会panic

return arrValues //.. 没问题,arrValues已经获得

}

英文:

Yes, it's totally acceptable. I usually use named returned variables to assure a default return in a defer error treatment, to ensure a minimum viable return, like example below:

//execute an one to one reflection + cache operation
func (cacheSpot CacheSpot) callOneToOne(originalIns []reflect.Value) (returnValue []reflect.Value) {

    defer func() { //assure for not panicking
	    if r := recover(); r != nil {
	   	    log.Error("Recovering! Error trying recover cached values!! y %v", r)

            //calling a original function
		    returnValue = reflect.ValueOf(cacheSpot.OriginalFunc).Call(originalIns)
	    }
    }()

    //... doing a really nasty reflection operation, trying to cache results. Very error prone. Maybe panic
    
    return arrValues //.. it's ok, arrValues achieved
}

huangapple
  • 本文由 发表于 2013年10月5日 14:19:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/19194691.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定