Golang 新手可能会踩的 50 个坑 上
左大括号 {
不能单独放一行
在其他大多数语言中,{
的位置你自行决定。Go 比较特别,遵守分号注入规则(automatic semicolon injection):编译器会在每行代码尾部特定分隔符后加 ;
来分隔多条语句,比如会在 )
后加分号:
1 | // 错误示例 |
未使用的变量
如果在函数体代码中有未使用的变量,则无法通过编译,不过全局变量声明但不使用是可以的。
即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它:
1 | // 错误示例 |
未使用的 import
如果你 import
一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。
可以使用 _
下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package
的 init()
简短声明的变量只能在函数内部使用
1 | // 错误示例 |
使用简短声明来重复声明变量
不能用简短声明方式来单独为一个变量重复声明, := 左侧至少有一个新变量,才允许多变量的重复声明:
1 | // 错误示例 |
不能使用简短声明来设置字段的值
struct
的变量字段不能使用 :=
来赋值以使用预定义的变量来避免解决:
1 | // 错误示例 |
不小心覆盖了变量
对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 :=
是一个赋值操作符。
如果你在新的代码块中像下边这样误用了 :=
,编译不会报错,但是变量不会按你的预期工作:
1 | func main() { |
这是 Go 开发者常犯的错,而且不易被发现。
可使用 vet
工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow
选项来启用:
1 | > go tool vet -shadow main.go |
注意 vet
不会报告全部被覆盖的变量,可以使用 go-nyet
来做进一步的检测:
1 | > $GOPATH/bin/go-nyet main.go |
显式类型的变量无法使用 nil 来初始化
nil
是 interface
、function
、pointer
、map
、slice
和 channel 类型变量的默认初始值。但声明时不指定类型,编译器也无法推断出变量的具体类型。
1 | // 错误示例 |
直接使用值为 nil 的 slice、map
1 | // map 错误示例 |
map 容量
在创建 map 类型的变量时可以指定容量,但不能像 slice
一样使用 cap()
来检测分配空间的大小:
1 | // 错误示例 |
string 类型的变量值不能为 nil
对那些喜欢用 nil
初始化字符串的人来说,这就是坑:
1 | // 错误示例 |
Array 类型的值作为函数参数
在 C/C++ 中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数内部会改变该数组的值。
在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:
1 | // 数组使用值拷贝传参 |
如果想修改参数数组:
- 接传递指向这个数组的指针类型:
1 | // 传址会修改原数据 |
- 直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)
1 | // 会修改 slice 的底层 array,从而修改 slice |
range 遍历 slice 和 array 时混淆了返回值
与其他编程语言中的 for-in
、foreach
遍历语句不同,Go 中的 range
在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值:
1 | // 错误示例 |
slice 和 array 其实是一维数据
看起来 Go 支持多维的 array 和 slice,可以创建数组的数组、切片的切片,但其实并不是。
对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用 Go 实现的效果并不理想。
可以使用原始的一维数组、“独立“ 的切片、“共享底层数组”的切片来创建动态的多维数组。
访问 map 中不存在的 key
和其他编程语言类似,如果访问了 map 中不存在的 key 则希望能返回 nil,比如在 PHP 中:
Go 则会返回元素对应数据类型的零值,比如 nil
、''
、false
和 0
,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。
检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:
1 | // 错误的 key 检测方式 |
string 类型的值是常量,不可更改
尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。
string
类型的值是只读的二进制 byte slice
,如果真要修改字符串中的字符,将 string
转为 []byte
修改后,再转为 string
即可:
1 | // 修改字符串的错误示例 |
注意: 上边的示例并不是更新字符串的正确姿势,因为一个 UTF8 编码的字符可能会占多个字节,比如汉字就需要 3~4 个字节来存储,此时更新其中的一个字节是错误的。
更新字串的正确姿势:将 string
转为 rune slice
(此时 1 个 rune 可能占多个 byte),直接更新 rune
中的字符
1 | func main() { |
string 与 byte slice 之间的转换
当进行 string 和 byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语的强制类型转换操作不同,也和新 slice 与旧 slice 共享底层数组不同。
Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:
- 在
map[string]
中查找key
时,使用了对应的[]byte
,避免做m[string(key)]
的内存分配 - 使用
for range
迭代string
转换为[]byte
的迭代:for i,v := range []byte(str) {...}
string 与索引操作符
对字符串用索引访问返回的不是字符,而是一个 byte
值。
这种处理方式和其他语言一样,比如 PHP 中:
1 | > php -r '$name="中文"; var_dump($name);' # "中文" 占用 6 个字节 |
1 | func main() { |
如果需要使用 for range
迭代访问字符串中的字符(unicode code point
/ rune
),标准库中有 "unicode/utf8"
包来做 UTF8 的相关解码编码。另外 utf8string
也有像 func (s *String) At(i int) rune
等很方便的库函数。
字符串并不都是 UTF8 文本
string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。
判断字符串是否是 UTF8 文本,可使用 "unicode/utf8"
包中的 ValidString()
函数:
1 | func main() { |
字符串的长度
在 Go 中:
1 | func main() { |
Go 的内建函数 len()
返回的是字符串的 byte
数量,而不是像 Python 中那样是计算 Unicode
字符数。
如果要得到字符串的字符数,可使用 "unicode/utf8"
包中的 RuneCountInString(str string) (n int)
1 | func main() { |
注意:RuneCountInString
并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune
:
1 | func main() { |