英文:
golang return multiple values issue
问题
我想知道为什么这段代码是有效的 Go 代码:
func FindUserInfo(id string) (Info, bool) {
it, present := all[id]
return it, present
}
但是这段代码却无效:
func FindUserInfo(id string) (Info, bool) {
return all[id]
}
有没有办法避免使用临时变量?
英文:
I was wondering why this is valid go code:
func FindUserInfo(id string) (Info, bool) {
it, present := all[id]
return it, present
}
but this isn't
func FindUserInfo(id string) (Info, bool) {
return all[id]
}
is there a way to avoid the temporary variables?
答案1
得分: 19
详细说明一下我的评论,Effective Go中提到,通过访问映射键进行多值赋值被称为“逗号-OK”模式。
有时候你需要区分一个缺失的条目和一个零值。对于“UTC”是否有条目,或者因为它根本不在映射中而是空字符串?你可以通过一种多重赋值的形式来区分。
var seconds int
var ok bool
seconds, ok = timeZone[tz]
出于明显的原因,这被称为“逗号-OK”习语。在这个例子中,如果tz存在,seconds将被适当设置,并且ok将为true;如果不存在,seconds将被设置为零,并且ok将为false。
我们可以看到,这与调用常规函数不同,编译器会告诉你出了什么问题:
package main
import "fmt"
func multiValueReturn() (int, int) {
return 0, 0
}
func main() {
fmt.Println(multiValueReturn)
asgn1, _ := multiValueReturn()
asgn2 := multiValueReturn()
}
在playground上,这将输出:
# command-line-arguments
/tmp/sandbox592492597/main.go:14: multiple-value multiValueReturn() in single-value context
这给了我们一个提示,这可能是编译器正在做的事情。在源代码中搜索“commaOk”一词,我们可以找到一些地方可以查看,包括types.unpack
在撰写本文时,该方法的godoc如下:
// unpack takes a getter get and a number of operands n. If n == 1, unpack
// calls the incoming getter for the first operand. If that operand is
// invalid, unpack returns (nil, 0, false). Otherwise, if that operand is a
// function call, or a comma-ok expression and allowCommaOk is set, the result
// is a new getter and operand count providing access to the function results,
// or comma-ok values, respectively. The third result value reports if it
// is indeed the comma-ok case. In all other cases, the incoming getter and
// operand count are returned unchanged, and the third result value is false.
//
// In other words, if there's exactly one operand that - after type-checking
// by calling get - stands for multiple operands, the resulting getter provides
// access to those operands instead.
//
// If the returned getter is called at most once for a given operand index i
// (including i == 0), that operand is guaranteed to cause only one call of
// the incoming getter with that i.
//
其中关键部分是该方法似乎确定了某个东西是否实际上是“逗号-OK”情况。
深入研究该方法告诉我们,它将检查操作数的模式是否为映射索引,或者模式是否设置为commaok
(在这里定义给出了许多关于何时使用它的提示,但在源代码中搜索对commaok
的赋值,我们可以看到它在从通道获取值和类型断言时使用)。记住后面加粗的部分!
if x0.mode == mapindex || x0.mode == commaok {
// comma-ok value
if allowCommaOk {
a := [2]Type{x0.typ, Typ[UntypedBool]}
return func(x *operand, i int) {
x.mode = value
x.expr = x0.expr
x.typ = a[i]
}, 2, true
}
x0.mode = value
}
allowCommaOk
是该函数的一个参数。查看在该文件中调用unpack
的位置,我们可以看到所有调用者都将false
作为参数传递。搜索代码库的其余部分,我们可以找到在Checker.initVars()
方法中的assignments.go
。
l := len(lhs)
get, r, commaOk := unpack(func(x *operand, i int) { check.expr(x, rhs[i]) }, len(rhs), l == 2 && !returnPos.IsValid())
由于似乎只能在进行多值赋值时使用“逗号-OK”模式来获取两个返回值,所以这似乎是查找的正确位置!在上面的代码中,检查左侧的长度,并且在调用unpack
时,allowCommaOk
参数是l == 2 && !returnPos.IsValid()
的结果。这里的!returnPos.IsValid()
有些令人困惑,因为这意味着该位置没有与之关联的文件或行信息,但我们将忽略它。
在该方法的后面,我们有:
var x operand
if commaOk {
var a [2]Type
for i := range a {
get(&x, i)
a[i] = check.initVar(lhs[i], &x, returnPos.IsValid())
}
check.recordCommaOkTypes(rhs[0], a)
return
}
那么这些告诉我们什么?
- 由于
unpack
方法接受一个allowCommaOk
参数,在除了assignment.go
的Checker.initVars()
方法之外的所有地方都被硬编码为false,我们可以假设只有在进行赋值并且左侧有两个变量时才会得到两个值。 unpack
方法将确定您是否实际上确实获得了一个ok
值,方法是检查您是否正在对切片进行索引、从通道获取值或进行类型断言。- 由于只有在进行赋值时才能获得
ok
值,所以在您的特定情况下,您总是需要使用变量。
英文:
To elaborate on my comment, the Effective Go mentions that the multi-value assignment from accessing a map key is called the "comma ok" pattern.
> Sometimes you need to distinguish a missing entry from a zero value. Is there an entry for "UTC" or is that the empty string because it's not in the map at all? You can discriminate with a form of multiple assignment.
var seconds int
var ok bool
seconds, ok = timeZone[tz]
> For obvious reasons this is called the “comma ok” idiom. In this example, if tz is present, seconds will be set appropriately and ok will be true; if not, seconds will be set to zero and ok will be false.
We can see that this differs from calling a regular function where the compiler would tell you that something is wrong:
package main
import "fmt"
func multiValueReturn() (int, int) {
return 0, 0
}
func main() {
fmt.Println(multiValueReturn)
asgn1, _ := multiValueReturn()
asgn2 := multiValueReturn()
}
On the playground this will output
# command-line-arguments
/tmp/sandbox592492597/main.go:14: multiple-value multiValueReturn() in single-value context
This gives us a hint that it may be something the compiler is doing. Searching the source code for "commaOk" gives us a few places to look, including types.unpack
At the time of writing this it this the method's godoc reads:
// unpack takes a getter get and a number of operands n. If n == 1, unpack
// calls the incoming getter for the first operand. If that operand is
// invalid, unpack returns (nil, 0, false). Otherwise, if that operand is a
// function call, or a comma-ok expression and allowCommaOk is set, the result
// is a new getter and operand count providing access to the function results,
// or comma-ok values, respectively. The third result value reports if it
// is indeed the comma-ok case. In all other cases, the incoming getter and
// operand count are returned unchanged, and the third result value is false.
//
// In other words, if there's exactly one operand that - after type-checking
// by calling get - stands for multiple operands, the resulting getter provides
// access to those operands instead.
//
// If the returned getter is called at most once for a given operand index i
// (including i == 0), that operand is guaranteed to cause only one call of
// the incoming getter with that i.
//
The key bits of this being that this method appears to determine whether or not something is actually a "comma ok" case.
Digging into that method tells us that it will check to see if the mode of the operands is indexing a map or if the mode is set to commaok
(where this is defined does give us many hints on when it's used, but searching the source for assignments to commaok
we can see it's used when getting a value from a channel and type assertions). Remember the bolded bit for later!
if x0.mode == mapindex || x0.mode == commaok {
// comma-ok value
if allowCommaOk {
a := [2]Type{x0.typ, Typ[UntypedBool]}
return func(x *operand, i int) {
x.mode = value
x.expr = x0.expr
x.typ = a[i]
}, 2, true
}
x0.mode = value
}
allowCommaOk
is a parameter to the function. Checking out where unpack
is called in that file we can see that all callers pass false
as an argument. Searching the rest of the repository leads us to assignments.go
in the Checker.initVars()
method.
l := len(lhs)
get, r, commaOk := unpack(func(x *operand, i int) { check.expr(x, rhs[i]) }, len(rhs), l == 2 && !returnPos.IsValid())
Since it seems that we can only use the "comma ok" pattern to get two return values when doing a multi-value assignment this seems like the right place to look! In the above code the length of the left hand side is checked, and when unpack
is called the allowCommaOk
parameter is the result of l == 2 && !returnPos.IsValid()
. The !returnPos.IsValid()
is somewhat confusing here as that would mean that the position has no file or line information associated with it, but we'll just ignore that.
Further down in that method we've got:
var x operand
if commaOk {
var a [2]Type
for i := range a {
get(&x, i)
a[i] = check.initVar(lhs[i], &x, returnPos.IsValid())
}
check.recordCommaOkTypes(rhs[0], a)
return
}
So what does all of this tell us?
- Since the
unpack
method takes anallowCommaOk
parameter that's hardcoded to false everywhere except inassignment.go
'sChecker.initVars()
method, we can probably assume that you will only ever get two values when doing an assignment and have two variables on the left-hand side. - The
unpack
method will determine whether or not you actually do get anok
value in return by checking if you are indexing a slice, grabbing a value from a channel, or doing a type assertion - Since you can only get the
ok
value when doing an assignment it looks like in your specific case you will always need to use variables
答案2
得分: 14
你可以通过使用命名返回值来节省一些按键次数:
func FindUserInfo(id string) (i Info, ok bool) {
i, ok = all[id]
return
}
但除此之外,我认为你想要的是不可能的。
英文:
You may save a couple of key strokes by using named returns:
func FindUserInfo(id string) (i Info, ok bool) {
i, ok = all[id]
return
}
But apart from that, I don't think what you want is possible.
答案3
得分: 8
简单来说,你的第二个示例不是有效的Go代码的原因是因为语言规范是这样规定的。
在赋值给两个变量的情况下,对映射进行索引只会产生一个附加的值。而返回语句并不是一个赋值操作。
> 在特殊形式的赋值或初始化中使用类型为 map[K]V 的映射 a 的索引表达式
>
> v, ok = a[x]
> v, ok := a[x]
> var v, ok = a[x]
>
> 会产生一个额外的无类型布尔值。如果键 x 存在于映射中,ok 的值为 true,否则为 false。
此外,对映射进行索引并不是一个“对多值函数的单次调用”,这是从函数返回值的三种方式之一(第二种方式,其他两种在这里不相关):
> 有三种方式可以从具有结果类型的函数返回值:
>
> 1. 返回值可以在“return”语句中显式列出。每个表达式必须是单值的,并且可赋值给函数结果类型的相应元素。
>
> 2. “return”语句中的表达式列表可以是对多值函数的单次调用。效果就好像从该函数返回的每个值都被分配给具有相应类型的临时变量,然后是一个“return”语句,列出这些变量,此时应用前一种情况的规则。
>
> 3. 如果函数的结果类型为其结果参数指定了名称,那么表达式列表可以为空。结果参数的行为类似于普通的局部变量,函数可以根据需要为它们赋值。此时,“return”语句返回这些变量的值。
至于你的实际问题:避免使用临时变量的唯一方法就是使用非临时变量,但通常这是不明智的,而且即使在安全的情况下也可能不是很优化。
那么,为什么语言规范不允许在返回语句中使用这种特殊的映射索引(或类型断言或通道接收,两者都可以利用“逗号 ok”习惯用法)?这是一个很好的问题。我猜测是为了保持语言规范的简洁性。
英文:
Simply put: the reason why your second example isn't valid Go code is because the language specification says so.
Indexing a map only yields a secondary value in an assignment to two variables. Return statement is not an assignment.
> An index expression on a map a of type map[K]V used in an assignment or initialization of the special form
>
> v, ok = a[x]
> v, ok := a[x]
> var v, ok = a[x]
>
> yields an additional untyped boolean value. The value of ok is true if the key x is present in the map, and false otherwise.
Furthermore, indexing a map is not a "single call to a multi-valued function", which is one of the three ways to return values from a function (the second one, the other two not being relevant here):
> There are three ways to return values from a function with a result type:
>
> 1. The return value or values may be explicitly listed in the "return" statement. Each expression must be single-valued and assignable to the corresponding element of the function's result type.
>
> 2. The expression list in the "return" statement may be a single call to a multi-valued function. The effect is as if each value returned from that function were assigned to a temporary variable with the type of the respective value, followed by a "return" statement listing these variables, at which point the rules of the previous case apply.
>
> 3. The expression list may be empty if the function's result type specifies names for its result parameters. The result parameters act as ordinary local variables and the function may assign values to them as necessary. The "return" statement returns the values of these variables.
As for your actual question: the only way to avoid temporary variables would be using non-temporary variables, but usually that would be quite unwise - and probably not much of an optimization even when safe.
So, why doesn't the language specification allow this kind of special use of map indexing (or type assertion or channel receive, both of which can also utilize the "comma ok" idiom) in return statements? That's a good question. My guess: to keep the language specification simple.
答案4
得分: 2
我不是Go专家,但我相信当你尝试返回数组return all[id]
时,你会遇到编译时错误。原因可能是因为函数的返回类型被特别指定为(Info, bool)
,而当你执行return all[id]
时,它无法将all[id]
的返回类型映射到(Info, bool)
。
然而,上面提到的解决方案中,被返回的变量i
和ok
与函数的返回类型(i Info, ok bool)
中提到的变量是相同的,因此编译器知道它返回的是什么,而不仅仅是(i Info, ok bool)
。
英文:
I'm no Go expert but I believe you are getting compile time error when you are trying to return the array i.e. return all[id]
. The reason could be because the functions return type is specially mentioned as (Info, bool)
and when you are doing return all[id]
it can't map the return type of all[id]
to (Info, bool)
.
However the solution mentioned above, the variables being returned i
and ok
are the same that are mentioned in the return type of the function (i Info, ok bool)
and hence the compiler knows what it's returning as opposed to just doing (i Info, ok bool)
.
答案5
得分: 1
默认情况下,Golang中的映射在访问键时返回单个值。
因此,对于期望返回两个值的函数,return all[id]
将无法编译。
参考链接:https://blog.golang.org/go-maps-in-action
英文:
By default, maps in golang return a single value when accessing a key
https://blog.golang.org/go-maps-in-action
Hence, return all[id]
won't compile for a function that expects 2 return values.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论