Go语言是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是Go本身的设计缺陷。如果你刚从其他语言转到Go,那这篇文章可能会帮到你。旨在帮助你跳过这些坑,能减少大量调试代码的时间。
1. 左大括号{
不能单独放一行
在其他大多数语言中,{
位置可以自行决定。Go比较特别,遵守分号注入规则(automatic semicolon injection) : 编译器会在没喊代码尾部特定分隔符后加 ;
来分隔多条语句,比如会在 )
后面加分号 :
// 错误示例
func main()
{
fmt.Println("hello world")
}
// 等效于
func main(); // 无函数体
{
fmt.Println("hello world")
}
./main.go : missing function body
./main.go : syntax error: unexpected semicolon or newline before {
// 正确示例
func main() {
fmt.Println("hello world")
}
2. 未使用的变量
如果在函数体代码中有未使用的变量,则无法编译通过,不过全局变量声明但是不使用是可以的。
即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它。
// 错误示例
var gVar int // 全局变量,声明不使用是可以的
func main() {
var one int // error : one declared and not used
two : = 2 // error : two declared and not used
var three int // error : three declared ant not used
three = 3
}
// 正确示例, 可以直接注释或移除未使用的变量
func main(){
var one int
_ = one
two := 2
fmt.Println(two)
var three int
one = three
var four int
four = four
}
3. 未使用的import
如果 import 一个包,但是包中的变量、函数、接口和结构体一个都没使用的话,将编译失败。
可以使用_
下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()
// 错误示例
import (
"fmt" // imported and not used : "fmt"
"log" // imported and not used : "log"
"time" // imported and not used : "time"
)
func main() {
}
// 正确示例, 可以使用goimports 工具来注释或移除未使用的包
import (
_ "fmt"
"lgo"
"time"
)
func main() {
_ = log.Println()
_ = time.Now
}
4. 简短声明的变量只能在函数内部使用
// 错误示例
tvar := 1 // syntax error : non-declaration statement outside function body
func main() {
}
// 正确示例
var tvar = 1
func main(){
}
5. 使用简短声明来重复声明变量
不能用简短声明方式来单独为一个变量重复声明, :=
左侧至少有一个新变量,才允许多变量的重复声明
// 错误示例
func main() {
one := 0
one := 1 // error : no new variables on left side of :=
}
// 正确示例
func main() {
one := 0
one, two := 1, 2 // two 是新变量,允许 one的重复声明。
one, two = two, one // 交换两个变量的值
}
6. 不能使用简短声明来设置字段的值
struct 的变量字段不能使用 :=
来赋值
// 错误示例
type info struct{
result int
}
func work() (int, error) {
return 2, nil
}
func main(){
var data info
data.result, err := work() // error : non-name data.result on left side of :=
fmt.Println("info : %+v\n", data)
}
// 正确示例
func main(){
var data info
var err error // err 需要预声明
data.result, err = work()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("info : %+v", data)
}
7. 不小心覆盖了变量
对从动态语言转过来的开发者来说,简短声明很好用,但可能会让人误会 :=
是一个赋值操作符。
如果在新的代码块中误用了:=
,编译不会报错,但是变量不会按照预期工作。
func main() {
x := 1
println(x) // 1
{
println(x) // 1
x := 2
println(x) // 2
}
println(x) // 1
}
这是Go开发者常犯的错,而且不易被发现。 可使用 vet 工具来诊断这种变量覆盖,Go默认不做覆盖检查,添加 -shadow
选项来启用:
go tool vet -shadow main.go
注意vet 不会报告全部覆盖的变量, 可以使用 go-nyet 来做进一步的检测。
8. 显式类型的变量无法使用nil来初始化
nil
是interface、function、pointer、map、slice和channel类型变量的默认初始值。 但声明时不指定类型,编译器也无法推断出变量的具体类型。
// 错误示例
func main(){
var x = nil // error : use of untyped nil
_ = x
}
// 正确示例
func main() {
var x = interface{} = nil
_ = x
}
9. 直接使用值为nil的slice、map
允许对值为nil的slice添加元素,但是对值为nil的map添加元素则会造成运行时panic。
// map错误示例
func main(){
var m map[int]int
m[1] = 1 // error: panic: assignment to entry in nil map
// m := make(map[int]int) // map的正确声明,分配实际内存
}
// slice 正确示例
func main() {
var s []int
s = append(s, 1)
}
10.map容量
在创建map类型的变量时可以指定容量,但是不能像slice一样使用 cap() 函数来检测分配空间的大小:
// 错误示例
func main(){
m := make(map[int]int, 100)
println(cap(m)) // error : invalid argument m(type map[int]int) for cap
}
11. String 类型的变量值不能为nil
// 错误示例
func main() {
var s string = nil // can not use nil as type string in assignment
if s == nil { // invalid operation: s == nil (mismatched types string and nil)
s = "default"
}
}
// 正确示例
func main() {
var s string // 字符串类型的零值默认是 ""
if s == "" {
s = "default"
}
}
12.Array类型的值作为函数参数
在C/C++中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数诶不会改变该数组的值。
在Go中,数组是指。作为参数传进函数时,传递的是数组的原始值的拷贝,此时在函数内部是无法更新该数组的。
// 数组使用值拷贝传参
func main() {
x := [3]int{1,2,3}
func(arr [3]int){
arr[0] = 7
fmt.Println(arr) // [7 2 3]
}(x)
fmt.Println(x) // [1 2 3] 并不是以为的 [7 2 3]
}
如何修改参数数组?
-
直接传递指向这个数组的指针类型
// 传址会修改原数据 func main() { x := [3]int{1,2,3} func(arr *[3]int) { (*arr)[0] = 7 fmt.Println(arr) // &[7,2,3] }(&x) fmt.Println(x) // [7 2 3] }
-
直接使用slice:即使函数内部的到的是slice的值拷贝,但依旧会更新slcie的原始数据(底层array)
// 会修改 slice 的底层array,从而修改 slice func main() { x := []int{1,2,3} func(arr []int) { arr[0] = 7 fmt.Println(arr) // &[7,2,3] }(x) fmt.Println(x) // [7 2 3] }
13. range 遍历 slice 和array 时混淆了返回值
与其他编程语言中 for -in
、 foreach
遍历语句不同,Go中的 range
在遍历时会生成两个值,第一个是元素的索引,第二个是元素的值:
// 错误示例
func main() {
x := []sting{"a","b","c"}
for v := range x {
fmt.Println(v) // 0 1 2
}
}
// 正确示例
func main() {
x := []sting{"a","b","c"}
for _, v := range x {
fmt.Println(v) // 0 1 2
}
}
14.slice和array其实是一维数据
看起来Go支持多维array和slice,可以创建数组的数组、切片的切片,但其实并不是。
对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用Go实现的效果并不理想。可以使用原始的一维数组、"独立"的切片、“共享底层数组”的切片来创建动态的多维数组。
- 使用原始的以为数组:要做好索引检查、溢出检测、以及当数组满时再添加值时要重新做内存分配。
- 使用“独立”的切片分两步:
- 创建外部slice
- 对每个内部slice进行内存分配
注意内部的slice 相互独立,使得任意内部slice增说都不会影响到其他slice
// 使用独立的slice创建[2][4]的动态多维数组
func main() {
x := 2
y := 4
table := make([][]int,x)
for i := arnge table {
table[i] = make([]int, y)
}
}
使用“共享底层数组”的切片
- 创建一个存放原始数据的容器slice
- 创建其他slice
- 切割原始slice来初始化其他slice
func main() {
h ,w := 2, 4
raw := make([]int, h*w)
for i := range raw {
raw[i] = i
}
// 初始化原始 slice
fmt.Println(raw, &raw[4]) // [0 1 2 3 4 5 6 7] 0xc420012120
table := make([][]int, h)
for i := range table {
// 等间距切割原始 slice,创建动态多维数组 table
// 0: raw[0*4: 0*4 + 4]
// 1: raw[1*4: 1*4 + 4]
table[i] = raw[i*w : i*w + w]
}
fmt.Println(table, &table[1][0]) // [[0 1 2 3] [4 5 6 7]] 0xc420012120
}
15.访问 map 中不存在的 key
和其他编程语言类似,如果访问了map中不存在的key希望返回nil。在Go中会返回元素对应数据类型的零值,比如 nil 、 '' 、 false 、0
,取值操作总有值返回,故不能通过取出来的值来判断key 是不是存在map中。检查key是否存在可以用map直接访问,检查返回的第二个参数即可:
// 错误的 key 检测方式
func main() {
x := map[string]string{"one" : "1", "two": "", "three" : "3"}
if v := x["two"]; v == "" {
fmt.Println("key two is no entry") // 键two存不存在都会返回空的字符串
}
}
// 正确示例
func main() {
x := map[string]string{"one" : "1", "two": "", "three" : "3"}
if _,ok := x["two"]; !ok {
fmt.Println("key two is no entry") // 键two存不存在都会返回空的字符串
}
}