11|代码块与作用域:如何保证变量不会被遮蔽?
Tony Bai

你好,我是 Tony Bai。
在上一节课,我们学习了变量的几种声明形式,还掌握了不同类型的变量应该采用哪种声明形式。在这一节课里,我们还是继续聊聊有关变量的事情。聊什么呢?别急,我们从一个 Go 变量遮蔽(Variable Shadowing)的问题说起。
什么是变量遮蔽呢?我们来看下面这段示例代码:
你可以看到,在这段代码中,函数 foo 调用前后,包级变量 a 的值都没有发生变化。这是因为,虽然 foo 函数中也使用了变量 a,但是 foo 函数中的变量 a 遮蔽了外面的包级变量 a,这使得包级变量 a 没有参与到 foo 函数的逻辑中,所以就没有发生变化了。
变量遮蔽是 Go 开发人员在日常开发工作中最容易犯的编码错误之一,它低级又不容易查找,常常会让你陷入漫长的调试过程。上面的实例较为简单,你可以通过肉眼很快找到问题所在,但一旦遇到更为复杂的变量遮蔽的问题,你就可能会被折腾很久,甚至只能通过工具才能帮助捕捉问题所在。
公开
同步至部落
取消
完成
0/2000
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Tony Bai · Go 语言第一课》,新⼈⾸单¥59
《Tony Bai · Go 语言第一课》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(47)
- 最新
- 精选
- lesserror关于这一块儿的知识其实还挺绕的。 不同代码块中的重名变量与变量重声明中的变量区别到底在哪儿?为了方便描述,我就把不同代码块中的重名变量叫做“可重名变量”吧。注意,在同一个代码块中不允许出现重名的变量,这违背了 Go 语言的语法。关于这两者的表象和机理,我们已经讨论得足够充分了。你现在可以说出几条区别?请想一想,然后再看下面的列表。 1. 变量重声明中的变量一定是在某一个代码块内的。注意,这里的“某一个代码块内”并不包含它的任何子代码块,否则就变成了“多个代码块之间”。而可重名变量指的正是在多个代码块之间由相同的标识符代表的变量。 2. 变量重声明是对同一个变量的多次声明,这里的变量只有一个。而可重名变量中涉及的变量肯定是有多个的。 3. 不论对变量重声明多少次,其类型必须始终一致,具体遵从它第一次被声明时给定的类型。而可重名变量之间不存在类似的限制,它们的类型可以是任意的。 4. 如果可重名变量所在的代码块之间,存在直接或间接的嵌套关系,那么它们之间一定会存在“屏蔽”的现象。但是这种现象绝对不会在变量重声明的场景下出现。
作者回复: 👍
711 - Amosヾ可不可以通过变量尽量不重名来避免变量遮蔽呢?
作者回复: 不重名肯定不会遮蔽。但是实际编码中,一些常用的功能变量,比如表示错误的err、表示下标的i,表示key和value的k、v等,如果要做不同命名,很容易在代码中出现大量的k1,v1,k2,v2等,阅读起来总是感觉缺少了一些优雅感。不知你是否有同感。
49 - 扣剑书生func checkYear() error { err := errors.New("wrong year") // 短变量形式,屏蔽了外层的包级变量 a,代替 其接收值 // err代替上面 的 err接收值 // 接收放在 switch 作用域外 a, err := getYear() switch a { case 2020: fmt.Println("哦哦哦it is", a, err) case 2021: fmt.Println("it is", a) err = nil } fmt.Println("after check, it is", a) return err }
作者回复: 👍
68 - 文经约定号包级别的变量用长的名字,越是局部的变量用越短小的名字,应该能够解决一大部分变量zhe遮蔽的问题。
作者回复: 也算是一个办法。前提是明确规则,且大家都遵守。这样才能在协作中,减少遮蔽问题的发生频度。
7 - 程旭阳go1.17.1 `type new int`会报错: cannot assign new to a (type int) in multiple assignment cannot use new value as type int in assignment 修改为 `type new = int` 之后不再报错 思考题解决方法: package main import ( "fmt" "errors" ) var a int = 2020 func checkYear() error { err := errors.New("wrong year") switch a, err = getYear(); a { case 2020: fmt.Println("it is", a, err) case 2021: fmt.Println("it is", a) err = nil } fmt.Println("after check, it is", a) return err } type year = int func getYear() (year, error) { var b int16 = 2021 return year(b), nil } func main() { err := checkYear() if err != nil { fmt.Println("call checkYear error:", err) return } fmt.Println("call checkYear ok") }
作者回复: 正确✅
36 - 子杨「作者回复: 一旦“禁止用预定义标识符定义新类型或者变量的行为”,那么new这样的预定义标识符就和关键字没啥区别了。」 想请问老师,预定义标识符和关键字的区别是啥?
作者回复: 预定义标识符可以被重新定义。 比如 var new int = 5 这时new就是一个变量。 但关键字不可以做标识符。 你不能用for作为变量名: var for int = 5 // error
归属地:辽宁4 - Rayjun修改两个地方,把 a 的类型改成 new,并 去掉 switch 那的一个引号 var a new = 2020 func checkYear() error { err := errors.New("wrong year") switch a, err = getYear(); a { case 2020: fmt.Println("it is", a, err) case 2021: fmt.Println("it is", a) err = nil } fmt.Println("after check, it is", a) return err } type new int func getYear() (new, error) { var b int16 = 2021 return new(b), nil } func main() { err := checkYear() if err != nil { fmt.Println("call checkYear error:", err) return } fmt.Println("call checkYear ok") }
作者回复: 有一点提醒一下:既然我们知道了new是预定义的标识符,我们在日常编写代码中尽量要避免重新定义new.
3 - 615刚细翻看了下go语言圣经,switch里的a和err确实是新声明的而不存在赋值行为,因为重新赋值行为只存在于变量已经在相同block中声明过。
作者回复: 正解✅
归属地:北京2 - 🐎感觉和js一样,变量就近使用
作者回复: 其实这是一个编程通用原则,利于提升可读性
归属地:北京2 - 史努比Universe Block是不是翻译成“全局代码块”更贴切一些,“宇宙代码块”总觉得怪怪的。
作者回复: 我倒是觉得“宇宙代码块”更形象生动罒ω罒。否则go官方也不会用universe block,而会用global block了。 另外代码块不要与作用域混淆。包代码块中声明的首字母大写的标识符实际是也是拥有全局作用域的,可以被任意其他代码所引用的。
2
收起评论