如何避免在Go语言编程中常见的那些容易忽略的细节问题?

摘要:可读性 Go 中 if else 应遵循快乐路径,即先考虑(退出返回跳过)错误情况,否则代码层级嵌套过重,可读性会变差。 作用域 Go 中的作用域分为显式作用域和隐式作用域。显示作用域是花括号包裹的代码块,隐式作用域定义如下: 宇宙(U
可读性 Go 中 if else 应遵循快乐路径,即先考虑(退出/返回/跳过)错误情况,否则代码层级嵌套过重,可读性会变差。 作用域 Go 中的作用域分为显式作用域和隐式作用域。显示作用域是花括号包裹的代码块,隐式作用域定义如下: 宇宙(Universe)代码块:所有Go源码都在该隐式代码块中,就相当于所有Go代码的最外层都存在一对大括号。 包代码块:每个包都有一个包代码块,其中放置着该包的所有Go源码。 文件代码块:每个文件都有一个文件代码块,其中包含着该文件中的所有Go源码。 作用域规则如下: 每个if、for和switch语句均被视为位于其自己的隐式代码块中。 switch或select语句中的每个子句都被视为一个隐式代码块。 Go标识符的作用域是基于代码块定义的,作用域规则描述了标识符在哪些代码块中是有效的。 标识符作用域规则如下: 预定义标识符,make、new、cap、len等的作用域范围是宇宙块。 顶层(任何函数之外)声明的常量、类型、变量或函数(但不是方法)对应的标识符的作用域范围是包代码块。比如:包级变量、包级常量的标识符的作用域都是包代码块。 Go源文件中导入的包名称的作用域范围是文件代码块。 方法接收器(receiver)​、函数参数或返回值变量对应的标识符的作用域范围是函数体(显式代码块)​,虽然它们并没有被函数体的大括号所显式包裹。 在函数内部声明的常量或变量对应的标识符的作用域范围始于常量或变量声明语句的末尾,止于其最里面的那个包含块的末尾。 在函数内部声明的类型标识符的作用域范围始于类型定义中的标识符,止于其最里面的那个包含块的末尾 if-else 根据上述作用域规则,给出以下代码示例: if a := 1; false { } else if b := 2; false { } else if c := 3; false { } else { fmt.Println(a, b, c) } 程序输出: 1, 2, 3 其作用域可以等价为: a := 1 if false { } else { { b := 2 if false { } else { { c := 3 if false { } else { fmt.Println(a, b, c) } } } } } for range 根据作用域规则给出以下示例: var m = [...]int{1, 2, 3, 4, 5} for i, v := range m { go func() { time.Sleep(3 * time.Second) fmt.Println(i, v) }() } time.Sleep(10 * time.Second) 在 Go 1.9 版本代码输出 4 5 4 5 4 5 4 5 4 5 从作用域的角度看 i 和 v 的作用域在 for 循环外,协程共用变量 i 和 v。在协程等待时,变量已经更新为 i=4,v=5,最终每个协程输出 4 和 5。 协程输出的并不都是最终值,主要原因是协程调度运行相比循环变量更新要慢。将 m 更新为长度为 5500 的切片就能看到协程打印的值是随机的。 解决方式可以从作用域入手,主要有两种: 方式 1 var m = [...]int{1, 2, 3, 4, 5} for i, v := range m { go func() { iCopy := i vCopy := v time.Sleep(3 * time.Second) fmt.Println(iCopy, vCopy) }() } time.Sleep(10 * time.Second) 将循环变量副本拷贝给闭包内变量,每个闭包(协程)有自己的副本变量。就是把作用域缩小到协程内了。
阅读全文