闭包
使用嵌入式函数时,作用域非常重要。函数中使用且未声明的任何变量都是对上层范围的引用。众所周知的使用 goroutine
的例子:
package main import ( "fmt" "time" ) func main() { for _, elem := range []byte{'a', 'b', 'c'} { go func() { fmt.Printf("%c\n", elem) }() } time.Sleep(1e9) // Sleeping to give time to the goroutines to be executed. }
运行该代码:
$ go run main.go c c c
这不是我们真正想要的。这是因为范围改变了 goroutine
中引用的 elem
,因此在短列表中,它将始终显示最后一个元素。
为了避免这种情况,有两种解决方案:
- 将变量传递给函数
package main import ( "fmt" "time" ) func main() { for _, elem := range []byte{'a', 'b', 'c'} { go func(char byte) { fmt.Printf("%c\n", char) }(elem) } time.Sleep(1e9) }
运行结果:
$ go run main.go a c b
- 在本地范围内创建变量的副本
package main import ( "fmt" "time" ) func main() { for _, elem := range []byte{'a', 'b', 'c'} { char := elem go func() { fmt.Printf("%c\n", char) }() } time.Sleep(1e9) }
运行该代码,可以得到我们想要的结果:
当我们将变量传递给函数时,我们实际上将变量的副本发送给以字符形式接收它的函数。因为每个 goroutine
都有自己的副本,所以没有问题。
当我们复制变量时,我们创建一个新变量并将 elem
的值分配给它。我们在每次迭代中都这样做,这意味着对于每个步骤,我们都会创建一个新变量,goroutine
会引用该变量。每个 goroutine
都有一个对不同变量的引用,并且它也可以正常工作。
现在,我们知道我们可以隐藏变量,为什么还要更改名称呢?我们可以简单地使用相同的名称,因为它会影响上层范围:
package main import ( "fmt" "time" ) func main() { for _, elem := range []byte{'a', 'b', 'c'} { go func(elem byte) { fmt.Printf("%c\n", elem) }(elem) } time.Sleep(1e9) }
package main import ( "fmt" "time" ) func main() { for _, elem := range []byte{'a', 'b', 'c'} { elem := elem go func() { fmt.Printf("%c\n", elem) }() } time.Sleep(1e9) }
当我们将变量传递给函数时,会发生同样的事情,我们将变量的副本传递给函数,该函数以名称 elem 和正确的值获取它。
在这个范围内,由于变量被遮蔽,我们无法从上层范围影响元素,所做的任何更改都将仅在此范围内应用。
当我们复制变量时,和以前一样:我们创建一个新变量并将 elem
的值分配给它。在这种情况下,新变量恰好与另一个变量具有相同的名称,但想法保持不变:新变量 + 赋值。当我们在范围内创建一个具有相同名称的新变量时,我们有效地隐藏了该变量,同时保持它的值。
:= 的情况
当 := 与多个返回函数(或类型断言、通道接收和映射访问)一起使用时,我们可以在 2 个语句中得到 3 个变量:
package main func main() { var iface interface{} str, ok := iface.(string) if ok { println(str) } buf, ok := iface.([]byte) if ok { println(string(buf)) } }
在这种情况下, ok
不会被遮蔽,它只是被覆盖。这就是为什么 ok
不能改变类型。但是,在范围内这样做会隐藏变量并允许使用不同的类型:
package main func main() { var m = map[string]interface{}{} elem, ok := m["test"] if ok { str, ok := elem.(string) if ok { println(str) } } }
总结
隐藏可能非常有用,但需要牢记以避免意外行为。它当然是基于案例的,它通常有助于提高可读性和安全性,但也可以减少它。
在 goroutines
的例子中,因为它是一个简单的例子,它的影子更具可读性,但在更复杂的情况下,最好使用不同的名称来确定你正在修改什么。然而,另一方面,尤其是对于错误,它是一个非常强大的工具。回到我的第一个例子:
package main import ( "io/ioutil" "log" ) func main() { f, err := ioutil.TempFile("", "") if err != nil { log.Fatal(err) } defer f.Close() if _, err := f.Write([]byte("hello world\n")); err != nil { err = nil } // err is still the one form TempFile }
在这种情况下,在 if
中隐藏 err
可以保证以前的错误不会受到影响,而如果使用相同的代码,我们在 if
中使用 =
而不是 :=
,它不会隐藏变量而是覆盖错误的值。