Golang混合赋值和声明

31

我开始用Go工作几周了,然后(再次)我遇到了一些对我来说似乎很奇怪的问题:

// Not working
a := 1
{
    a, b := 2, 3
}

// Works
a := 1
a, b := 2, 3

playground

我想同时为两个变量赋值。 其中一个已经在上层作用域中声明,而另一个没有。

这样是不行的:编译器会试图重新声明前一个变量。 然而,如果该变量在同一作用域中声明,则可以正常工作。

为什么会这样呢?


prog.go:11: a declared and not used - squiguy
我将其放在括号下方:fmt.Println(a)。我不明白为什么在括号内部重新声明了 a - Procrade
可能是["declared and not used" Error]的重复问题。 (https://dev59.com/mXXYa4cB1Zd3GeqP9LGr) - Jonathan Hall
4个回答

26
你正在经历的通常被称为"变量遮蔽"。当你在内部作用域中使用:=与任何变量,包括在iffor语句中,尽管缺少大括号,都会关联一个新值和类型到该变量:
n := "Example"
//Prints the string variable `n` to standard output and
// returns the number of bytes written in int variable `n` and
// an error indicator in error variable `err`.
if n, err := fmt.Println(n); err != nil {
    panic(err)
} else {
    fmt.Println(n, "bytes written")
}

//Prints the string variable `n` to standard output.
fmt.Printf("n = %q\n", n)

输出:

Example
8 bytes written
n = "Example"

有几种不同的方法可以解决这个问题:
  • 在使用变量之前先声明需要的变量,并使用普通赋值=
  • 使用不同的变量名称
  • 创建一个新的作用域并保存变量的值以便以后访问,使用你想要的变量名与:=,在作用域结束之前,恢复该值;通常更容易只是使用不同的变量名称,因为你已经创建了另一个变量。

相反的情况也可能发生,即在内部范围中声明某些内容却没有意识到:

if _, err := fmt.Println(n); err != nil {
    panic(err)
} else {
    fmt.Println(n, "bytes written")
}

//undefined: err
if _, err = fmt.Println(n); err != nil {
    //undefined: err
    panic(err)
}

解决这个问题有几种不同的方法:

  • 在使用变量之前声明所需变量,并使用普通赋值符号=
  • 将第一个:=if语句分开,使变量得到预期的声明;这允许您在该作用域及其任何封闭作用域中的所有其他实例中使用=
  • 更改所有=的实例为:=以修复错误

请注意,当函数返回多个值时,您可能会在最后两种情况中遇到变量屏蔽问题,但可以按上述方式解决。

在Go Playground上尝试这两个示例。

你的最后一个例子展示了声明和初始化新变量b,同时给现有变量a赋值。没有创建新的作用域,所以你没有遮蔽原始变量a,你可以通过在每次分配之后(但在下一次声明/分配之前)打印a的地址来验证
a := 1
fmt.Println(&a)
a, b := 2, 3
fmt.Println(&a)
a = b          // avoids a "declared but not used" error for `b`

当然,如果你没有声明b,那么编译器会报错,因为第二个声明左边没有新的变量,这是说你试图在同一作用域内两次声明a
请注意,如果小心应用此想法,还可以用于查找被遮蔽的变量。例如,在你的示例中,“不起作用”的代码将打印a的不同地址, 这取决于内部作用域中的a是否已经被声明:
a := 1
{
    fmt.Println(&a)    // original `a`
    a, b := 2, 3
    fmt.Println(&a)    // new `a`
    a = b              // avoids a "declared but not used" error for `b`
}
fmt.Println(&a)        // original `a`

问题示例的最后一部分在看起来是相同范围(未引入新的括号或缩进)上两次使用了:=。你的回答很好地解释了当条件或循环(if或for)添加新作用域时会发生什么,但是连续两个声明在相同级别上会怎样呢?a,b := 2,3是否会为整个块遮盖先前语句中的a声明? - init_js
@init_js 已更新以解决该问题。感谢您发现我的疏忽。 - user539810

9
根据golang文档

在一个块中声明的标识符可以在内部块中被重新声明。

这正是你的示例所显示的,由于“:=”,a在括号内被重新声明,却从未使用过。

解决方法是同时声明变量并使用它:

var a, b int
{
    b, a = 2, 3
    fmt.Println(b)
}
fmt.Println(a)

嗯,在某些情况下,这对我来说并没有意义,在这种情况下,我不会在内部块之外使用b,因此将其声明在内部更为合理。否则,我可以声明一个临时变量并将a赋值给它...这两种解决方案对我来说都有些奇怪。 - Procrade
是的,如果b只在一个作用域内使用,那么它必须在该作用域内声明。但是,如果你需要在作用域外使用b,那么就要在之前声明它。不管怎样,首先,你不应该声明内联块,没有理由这样做,或者我漏掉了什么? - jnmoal

3

你的问题有两个部分:
第一部分:
= 是赋值操作符
:= 是在函数块内(不是全局)定义并赋值于至少一个新的变量,下面是示例:

package main

import (
    "fmt"
)

func main() {
    var u1 uint32      //declare a variable and init with 0
    u1 = 32            //assign its value
    var u2 uint32 = 32 //declare a variable and assign its value at once
    //declare a new variable with defining data type:
    u3 := uint32(32)        //inside the function block this is equal to: var u3 uint32 = 32
    fmt.Println(u1, u2, u3) //32 32 32
    //u3 := 20//err: no new variables on left side of :=
    u3 = 20
    fmt.Println(u1, u2, u3)       //32 32 20
    u3, str4 := 100, "str"        // at least one new var
    fmt.Println(u1, u2, u3, str4) //32 32 100 str
}

第二部分:
在一个块中声明的标识符可能会在内部块中重新声明。
这里有四个关于变量作用域和遮蔽的不同工作示例:

限制变量作用域的简单方法:

package main
import "fmt"
func main() {
    i := 1
    j := 2
    //new scope :
    {
        i := "hi" //new local var
        j++
        fmt.Println(i, j) //hi 3
    }
    fmt.Println(i, j) //1 3
}

使用函数调用限制变量作用域:

package main
import "fmt"
func fun(i int, j *int) {
    i++                //+nice: use as local var without side effect
    *j++               //+nice: intentionally use as global var
    fmt.Println(i, *j) //11 21
}
func main() {
    i := 10 //scope: main
    j := 20
    fun(i, &j)
    fmt.Println(i, j) //10 21
}

在语句中使用简写赋值:

package main
import "fmt"
func main() {
    i := 10 //scope: main
    j := 4
    for i := 'a'; i < 'b'; i++ {
        fmt.Println(i, j) //97 4
    }
    fmt.Println(i, j) //10 4

    if i := "test"; len(i) == j {
        fmt.Println(i, j) // i= test , j= 4
    } else {
        fmt.Println(i, j) //test 40
    }
    fmt.Println(i, j) //10 4
}

全局变量的阴影化:

package main
import "fmt"
var i int = 1 //global
func main() {
    j := 2
    fmt.Println(i, j) //1 2
    i := 10           //Shadowing global var
    fmt.Println(i, j) //10 2
    fun(i, j)         //10 2
}
func fun(i, j int) {
    //i := 100   //no new variables on left side of :=
    fmt.Println(i, j) //10 2
}

1
简而言之:当a, b = 2, 3表示“同时赋值”,a, b := 2, 3表示“声明并同时赋值”时,如果你需要对一个变量赋值并对另一个变量进行声明和赋值,则解决方法是先声明该变量,然后再同时赋值。
a := 1
{
    var b int
    a, b = 2, 3
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接