Golang 新手要注意的陷阱和常见错误(一)

码农编程进阶笔记

共 7792字,需浏览 16分钟

 · 2020-08-20

Go 是一门简单有趣的语言,但与其他语言类似,它会有一些技巧。。。这些技巧的绝大部分并不是 Go 的缺陷造成的。如果你以前使用的是其他语言,那么这其中的有些错误就是很自然的陷阱。其它的是由错误的假设和缺少细节造成的。

如果你花时间学习这门语言,阅读官方说明、wiki、邮件列表讨论、大量的优秀博文和 Rob Pike 的展示,以及源代码,这些技巧中的绝大多数都是显而易见的。尽管不是每个人都是以这种方式开始学习的,但也没关系。如果你是 Go 语言新人,那么这里的信息将会节约你大量的调试代码的时间。

一. 初级篇

1. 开大括号不能放在单独的一行

在大多数其他使用大括号的语言中,你需要选择放置它们的位置。Go 的方式不同。你可以为此感谢下自动分号的注入(没有预读)。是的, Go 中也是有分号的:-)
失败的例子:

package mainimport "fmt"func main()  { //error, can't have the opening brace on a separate line    fmt.Println("hello there!")}

编译错误:

/tmp/sandbox826898458/main.go:6: syntax error: unexpected semicolon or newline before {

有效的例子:

package mainimport "fmt"func main() {      fmt.Println("works!")}


2. 未使用的变量

如果你有未使用的变量,代码将编译失败。当然也有例外。在函数内一定要使用声明的变量,但未使用的全局变量是没问题的。
如果你给未使用的变量分配了一个新的值,代码还是会编译失败。你需要在某个地方使用这个变量,才能让编译器愉快的编译。
Fails:

package mainvar gvar int //not an errorfunc main() {      var one int   //error, unused variable    two := 2      //error, unused variable    var three int //error, even though it's assigned 3 on the next line    three = 3     }

Compile Errors:

/tmp/sandbox473116179/main.go:6: one declared and not used/tmp/sandbox473116179/main.go:7: two declared and not used/tmp/sandbox473116179/main.go:8: three declared and not used

Works:

package mainimport "fmt"func main() {      var one int    _ = one    two := 2     fmt.Println(two)    var three int     three = 3    one = three    var four int    four = four}

另一个选择是注释掉或者移除未使用的变量 


3. 未使用的 import

如果你引入一个包,而没有使用其中的任何函数、接口、结构体或者变量的话,代码将会编译失败。
你可以使用 goimports 来增加引入或者移除未使用的引用:

$ go get golang.org/x/tools/cmd/goimports

如果你真的需要引入的包,你可以添加一个下划线标记符 _ ,来作为这个包的名字,从而避免编译失败。下滑线标记符用于引入,但不使用。

package mainimport (    "fmt"    "log"    "time")func main() {}
Compile Errors:
/tmp/sandbox627475386/main.go:4: imported and not used: "fmt"/tmp/sandbox627475386/main.go:5: imported and not used: "log"/tmp/sandbox627475386/main.go:6: imported and not used: "time"

Works:

package mainimport (    _ "fmt"    "log"    "time")var _ = log.Printlnfunc main() {    _ = time.Now}

另一个选择是移除或者注释掉未使用的 import

4. 简式的变量声明仅可以在函数内部使用

Fails:

package mainmyvar := 1 //errorfunc main() {}

Compile Error:

/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body

Works:

package mainvar myvar = 1func main() {}

5. 使用简式声明重复声明变量

你不能在一个单独的声明中重复声明一个变量,但在多变量声明中这是允许的,其中至少要有一个新的声明变量。
重复变量需要在相同的代码块内,否则你将得到一个隐藏变量。
Fails:

package mainfunc main() {    one := 0    one := 1 //error}

Compile Error:

/tmp/sandbox706333626/main.go:5: no new variables on left side of :=

Works:

package mainfunc main() {    one := 0    one, two := 1,2    one,two = two,one}

6. 偶然的变量隐藏 Accidental Variable Shadowing

短式变量声明的语法如此的方便(尤其对于那些使用过动态语言的开发者而言),很容易让人把它当成一个正常的分配操作。如果你在一个新的代码块中犯了这个错误,将不会出现编译错误,但你的应用将不会做你所期望的事情。

package mainimport "fmt"func main() {    x := 1    fmt.Println(x)     //prints 1    {        fmt.Println(x) //prints 1        x := 2        fmt.Println(x) //prints 2    }    fmt.Println(x)     //prints 1 (bad if you need 2)}
即使对于经验丰富的Go开发者而言,这也是一个非常常见的陷阱。这个坑很容易挖,但又很难发现。

你可以使用 vet 命令来发现一些这样的问题。默认情况下, vet不会执行这样的检查,你需要设置 -shadow参数:
go tool vet -shadow your_file.go


7. 不使用显式类型,无法使用“nil”来初始化变量

nil 标志符用于表示 interface 、函数、 mapssliceschannels 的“零值”。如果你不指定变量的类型,编译器将无法编译你的代码,因为它猜不出具体的类型。
Fails:

package mainfunc main() {    var x = nil //error    _ = x}
Compile Error:
/tmp/sandbox188239583/main.go:4: use of untyped nil

Works:

package mainfunc main() {    var x interface{} = nil    _ = x}

8. 使用“nil” Slices and Maps

在一个 nilslice 中添加元素是没问题的,但对一个 map 做同样的事将会生成一个运行时的 panic
Works:

package mainfunc main() {    var s []int    s = append(s,1)}

Fails:

package mainfunc main() {    var m map[string]int    m["one"] = 1 //error}

9. map的容量

你可以在 map 创建时指定它的容量,但你无法在 map 上使用 cap() 函数。
Fails:

package mainfunc main() {    m := make(map[string]int,99)    cap(m) //error}

Compile Error:

/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]intfor cap


10. 字符串不会为nil

这对于经常使用 nil 分配字符串变量的开发者而言是个需要注意的地方。
Fails:

package mainfunc main() {    var x string = nil //error    if x == nil { //error        x = "default"    }}
Compile Errors:
/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)

Works:

package mainfunc main() {    var x string //defaults to "" (zero value)    if x == "" {        x = "default"    }}

11. array 函数的参数

如果你是一个 C 或则 C++ 开发者,那么数组对你而言就是指针当你向函数中传递数组时,函数会参照相同的内存区域,这样它们就可以修改原始的数据

Go 中的数组是数值,因此当你向函数中传递数组时,函数会得到原始数组数据的一份复制。如果你打算更新数组的数据,这将会是个问题。

package mainimport "fmt"func main() {    x := [3]int{1,2,3}    func(arr [3]int) {        arr[0] = 7        fmt.Println(arr) //prints [7 2 3]    }(x)    fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])}

如果你需要更新原始数组的数据,你可以使用数组指针类型。

package mainimport "fmt"func main() {    x := [3]int{1,2,3}    func(arr *[3]int) {        (*arr)[0] = 7        fmt.Println(arr) //prints &[7 2 3]    }(&x)    fmt.Println(x) //prints [7 2 3]}

另一个选择是使用 slice 。即使你的函数得到了 slice 变量的一份拷贝,它依旧会参照原始的数据。

package mainimport "fmt"func main() {    x := []int{1,2,3}    func(arr []int) {        arr[0] = 7        fmt.Println(arr) //prints [7 2 3]    }(x)    fmt.Println(x) //prints [7 2 3]}


12. 在 slice 和 array 使用“range”语句时的出现的不希望得到的值

如果你在其他的语言中使用 for-in 或者 foreach 语句时会发生这种情况。Go 中的 range 语法不太一样。它会得到两个值:第一个值是元素的索引,而另一个值是元素的数据。
Bad:

package mainimport "fmt"func main() {    x := []string{"a","b","c"}    for v := range x {        fmt.Println(v) //prints 0, 1, 2    }}

Good:

package mainimport "fmt"func main() {    x := []string{"a","b","c"}    for _, v := range x {        fmt.Println(v) //prints a, b, c    }}


13. slices 和 arrays 是一维的

看起来 Go 好像支持多维的 ArraySlice ,但不是这样的。尽管可以创建数组的数组或者切片的切片。对于依赖于动态多维数组的数值计算应用而言, Go 在性能和复杂度上还相距甚远。

你可以使用纯一维数组、“独立”切片的切片,“共享数据”切片的切片来构建动态的多维数组。
如果你使用纯一维的数组,你需要处理索引、边界检查、当数组需要变大时的内存重新分配。

使用“独立” slice 来创建一个动态的多维数组需要两步。首先,你需要创建一个外部的 slice 。然后,你需要分配每个内部的 slice 。内部的 slice 相互之间独立。你可以增加减少它们,而不会影响其他内部的 slice

package mainfunc main() {    x := 2    y := 4    table := make([][]int,x)    for i:= range table {        table[i] = make([]int,y)    }}

使用“共享数据” sliceslice 来创建一个动态的多维数组需要三步。首先,你需要创建一个用于存放原始数据的数据“容器”。然后,你再创建外部的 slice 。最后,通过重新切片原始数据 slice 来初始化各个内部的 slice

package mainimport "fmt"func main() {    h, w := 2, 4    raw := make([]int,h*w)    for i := range raw {        raw[i] = i    }    fmt.Println(raw,&raw[4])    //prints: [0 1 2 3 4 5 6 7]     table := make([][]int,h)    for i:= range table {        table[i] = raw[i*w:i*w + w]    }    fmt.Println(table,&table[1][0])    //prints: [[0 1 2 3] [4 5 6 7]] }

关于多维 arrayslice 已经有了专门申请,但现在看起来这是个低优先级的特性。


14. 访问不存在的 map keys

这对于那些希望得到 nil 标示符的开发者而言是个技巧(和其他语言中做的一样)。如果对应的数据类型的“零值”是 nil ,那返回的值将会是 nil ,但对于其他的数据类型是不一样的。检测对应的“零值”可以用于确定 map 中的记录是否存在,但这并不总是可信(比如,如果在二值的 map 中“零值”是 false ,这时你要怎么做)。检测给定 map 中的记录是否存在的最可信的方法是,通过 map 的访问操作,检查第二个返回的值。
Bad:

package mainimport "fmt"func main() {    x := map[string]string{"one":"a","two":"","three":"c"}    if v := x["two"]; v == "" { //incorrect        fmt.Println("no entry")    }}

Good:

package mainimport "fmt"func main() {    x := map[string]string{"one":"a","two":"","three":"c"}    if _,ok := x["two"]; !ok {        fmt.Println("no entry")    }}


15. Strings 无法修改

尝试使用索引操作来更新字符串变量中的单个字符将会失败。string 是只读的 byte slice (和一些额外的属性)。如果你确实需要更新一个字符串,那么使用 byte slice ,并在需要时把它转换为 string 类型。
Fails:

package mainimport "fmt"func main() {    x := "text"    x[0] = 'T'    fmt.Println(x)}
Compile Error:
/tmp/sandbox305565531/main.go:7: cannot assign to x[0]

Works:

package mainimport "fmt"func main() {    x := "text"    xbytes := []byte(x)    xbytes[0] = 'T'    fmt.Println(string(xbytes)) //prints Text}
需要注意的是:这并不是在文字 string 中更新字符的正确方式,因为给定的字符可能会存储在多个 byte 中。如果你确实需要更新一个文字 string ,先把它转换为一个 rune slice

即使使用 rune slice ,单个字符也可能会占据多个 rune ,比如当你的字符有特定的重音符号时就是这种情况。这种复杂又模糊的“字符”本质是 Go 字符串使用 byte 序列表示的原因。

浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报