写点什么

Go 语言入门很简单:Go 中的作用域和变量隐藏

作者:宇宙之一粟
  • 2022 年 4 月 25 日
  • 本文字数:2969 字

    阅读完需:约 10 分钟

Go语言入门很简单:Go 中的作用域和变量隐藏

变量隐藏在 Go 中可能会令人困惑,让我们尝试弄清楚。


package main
import ( "fmt" "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 { log.Fatal(err) } else { fmt.Println("All success") }}
复制代码


请注意,我们首先从 TempFile 函数创建了两个变量:ferr。然后我们调用 Write 丢弃写入的字节数。我们让函数在 if 语句中调用它。让我们编译,它工作正常。

$ go run main.goAll success
复制代码


现在,与 if 之外的 Write 调用相同的代码:

package main
import ( "fmt" "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 { // log.Fatal(err) // } else { // fmt.Println("All success") // }
_, err := f.Write([]byte("Hello World\n")) if err != nil { log.Fatal(err) } else { fmt.Println("All success") }}
复制代码

运行该代码:

$ go run main.go# command-line-arguments./main.go:23:9: no new variables on left side of :=
复制代码


所以发生了什么事?

请注意,我们使用 := 调用 Write,这意味着我们创建了一个新变量 err。在第二个例子中,很明显,err 已经存在,所以我们不能重新声明它。

但是为什么它第一次起作用呢?因为在 Go 中,变量是其作用域的本地变量。在第一个示例中,我们实际上在 if 范围内隐藏了 err


例如:

package main
func main() { var err error _ = err var err error _ = err}
复制代码

这显然会失败,但是,如果我们限定第二个 err,它会起作用!


package main
func main() { var err error _ = err { var err error _ = err }}
复制代码


包隐藏

考虑以下代码:

package main
import "fmt"
func Debugf(fmt string, args ...interface{}) { fmt.Printf(fmt, args...)}
复制代码

起初,它看起来不错。我们从 fmt 包中调用 Printf 并将 fmt 变量传递给它。


函数声明中的 fmt 字符串实际上隐藏了包,现在“只是”一个变量。编译器会抱怨:我们需要使用不同的变量名来保存对 fmt 包的访问。


全局变量

需要考虑的是,一个函数已经是一个“子作用域”,它是全局作用域内的一个作用域。这意味着您在函数中声明的任何变量都可以在全局范围内隐藏某些内容。


正如我们之前看到的,变量可以映射包,全局变量和函数的概念是相同的。


类型强制

就像我们可以用变量或函数来映射一个包一样,我们也可以用任何类型的新变量来映射一个变量。阴影变量不需要来自同一类型。这个例子编译得很好:

package main
func main() { var a string _ = a { var a int _ = a }}
复制代码

闭包

使用嵌入式函数时,作用域非常重要。函数中使用且未声明的任何变量都是对上层范围的引用。众所周知的使用 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.goccc
复制代码

这不是我们真正想要的。这是因为范围改变了 goroutine 中引用的 elem,因此在短列表中,它将始终显示最后一个元素。


为了避免这种情况,有两种解决方案:

  1. 将变量传递给函数

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.goacb
复制代码
  1. 在本地范围内创建变量的副本

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 中使用 = 而不是 :=,它不会隐藏变量而是覆盖错误的值。


参考链接:Scope and Shadowing in Go - Guillaume's Thoughts (charmes.net)

发布于: 2022 年 04 月 25 日阅读数: 21
用户头像

宇宙古今无有穷期,一生不过须臾,当思奋争 2020.05.07 加入

🏆InfoQ写作平台-第二季签约作者 🏆 混迹于江湖,江湖却没有我的影子 热爱技术,专注于后端全栈,轻易不换岗 拒绝内卷,工作于软件工程师,弹性不加班 热衷分享,执着于阅读写作,佛系不水文

评论

发布
暂无评论
Go语言入门很简单:Go 中的作用域和变量隐藏_作用域_宇宙之一粟_InfoQ写作社区