golang易错题收集
1. 变量作用域
var p *int func foo() (*int, error) { var i int = 5 return &i, nil } func bar() { //use p fmt.Println(*p) } func main() { p, err := foo() if err != nil { fmt.Println(err) return } bar() fmt.Println(*p) }
runtime error
问题出在操作符:=,对于使用:=定义的变量,如果新变量与同名已定义的变量不在同一个作用域中,那么 Go 会新定义这个变量。对于本例来说,main() 函数里的 p 是新定义的变量,会遮住全局变量 p,导致执行到bar()时程序,全局变量 p 依然还是 nil,程序随即 Crash。
2. 循环次数在循环开始前就已经确定
func main() { v := []int{1, 2, 3} for i := range v { v = append(v, i) } }
循环次数在循环开始前就已经确定,循环内改变切片的长度,不影响循环次数。
[1 2 3 0]
[1 2 3 0 1]
[1 2 3 0 1 2]
func main() { var a = [5]int{1, 2, 3, 4, 5} var r [5]int for i, v := range a { if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v } fmt.Println("r = ", r) fmt.Println("a = ", a) }
range 表达式是副本参与循环,就是说例子中参与循环的是 a 的副本,而不是真正的 a。
r = [1 2 3 4 5] a = [1 12 13 4 5]
3. goroutine访问外部变量
func main() { var m = [...]int{1, 2, 3} for i, v := range m { go func() { fmt.Println(i, v) }() } time.Sleep(time.Second * 3) }
for range 使用短变量声明(:=)的形式迭代变量,需要注意的是,变量 i、v 在每次循环体中都会被重用,而不是重新声明。
各个 goroutine 中输出的 i、v 值都是 for range 循环结束后的 i、v 最终值(goroutines启动时for早已执行完),而不是各个goroutine启动时的i, v值。
可以理解为闭包引用,使用的是上下文环境的值。
两种可行的 fix 方法:
for i, v := range m { go func(i,v int) { fmt.Println(i, v) }(i,v) } // 临时变量保存当前值 for i, v := range m { i := i // 这里的 := 会重新声明变量,而不是重用 v := v go func() { fmt.Println(i, v) }() }
注意:函数的参数或函数的接收者是及时计算值并压栈的(如9 GMP模型)。
一个是函数的参数或接收者(立即求值),一个是函数的执行体(执行体不会立即执行),defer同样。
package main import ( "fmt" "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) var a = []int{1, 2, 3, 4} for _, v := range a { go fmt.Println(v) } time.Sleep(3 * time.Second) }
4 1 2 3
4. defer
func f(n int) (r int) { defer func() { r += n recover() }() var f func() defer f() f = func() { r += 2 } return n + 1 } func main() { fmt.Println(f(3)) }
7
第一步执行r = n +1,接着执行第二个 defer,由于此时 f() 未定义,引发异常,随即执行第一个 defer,异常被 recover(),程序正常执行,最后 return。
参考:5 年 Gopher 都不知道的 defer 细节,你别再掉进坑里!
5. slice
func change(s ...int) { s = append(s,3) } func main() { slice := make([]int,5,5) slice[0] = 1 slice[1] = 2 change(slice...) fmt.Println(slice) change(slice[0:2]...) fmt.Println(slice) }
[1 2 0 0 0]
[1 2 3 0 0]
知识点:可变函数、append()操作。Go 提供的语法糖...,可以将 slice 传进可变函数,不会创建新的切片。第一次调用 change() 时,append() 操作使切片底层数组发生了扩容,原 slice 的底层数组不会改变;第二次调用change() 函数时,使用了操作符[i,j]获得一个新的切片,假定为 slice1,它的底层数组和原切片底层数组是重合的,不过 slice1 的长度、容量分别是 2、5,所以在 change() 函数中对 slice1 底层数组的修改会影响到原切片。
6. interface比较
type T interface{} type X string type Y = string func main() { var t T = "abc" var x X = "abc" var y Y = "abc" fmt.Println(t == x) fmt.Println(t == string(x)) fmt.Println(t == y) fmt.Println(t == string(y)) } // false true true true
接口值(t)与非接口值(x)也是可以比较的, Go语言规范里(https://golang.org/ref/spec#Comparison_operators):
A value x of non-interface type X and a value t of interface type T are comparable when values of type X are comparable and X implements T.
They are equal if t's dynamic type is identical to X and t's dynamic value is equal to x.
意思是如果非接口值x实现了T接口, 而且接口值的动态类型与X类型相同, 动态值与x相等, 那比较结果就相等.
而这一行中, 接口值t的动态类型为string, 动态值为"abc"(第12行), 而非接口值x的类型为X, 类型都不相同, 动态值就不用比较了
把==号后面的值转换为string类型, 再与接口值t的动态类型和动态值比较, 类型相同, 值相等, 返回true
接口值t的动态类型和非接口值y的类型比较, 相同, 动态值与非接口值y的值比较, 相等, 返回true
Y是string类型的别名, 所以string(y)和第19行的y一样, 都是string类型, 此行中的string()没有必要
7. 多重赋值
func main() { i := 1 s := []string{"A", "B", "C"} i, s[i-1] = 2, "Z" fmt.Printf("s: %v \n", s) // s: [Z B C] }
多重赋值分为两个步骤,有先后顺序:
计算等号左边的索引表达式和取址表达式,接着计算等号右边的表达式;
赋值;
8. 指针接收直接引用,值接收拷贝
type People struct { Value int name string } func (s *People) valueAdd1() { s.Value += 1 fmt.Println("in *People: ", s.Value, ", addr:", &s.Value) } func (s People) valueAdd2() { s.Value += 1 fmt.Println("in People: ", s.Value, ", addr:", &s.Value) } func (s People) valueAdd3() People { s.Value += 1 fmt.Println("in People return: ", s.Value, ", addr:", &s.Value) return s } func main() { fmt.Printf("%T\n", &People{}) people := new(People) fmt.Println("addr:", &people.Value) people.valueAdd1() fmt.Println(people.Value) people.valueAdd2() fmt.Println(people.Value) people2 := people.valueAdd3() fmt.Println(people2.Value) } //// *main.People addr: 0xc0000044e0 in *People: 1 , addr: 0xc0000044e0 1 in People: 2 , addr: 0xc000004500 1 in People return: 2 , addr: 0xc000004520 2
9. GMP模型
package main import ( "fmt" "runtime" "time" ) type field struct { name string } func (f field) print() { fmt.Println(f.name) } func main() { runtime.GOMAXPROCS(1) data := []*field{{"one"}, {"two"}, {"three"}} for _, v := range data { go v.print() } time.Sleep(3 * time.Second) }
当设置GOMAXPROCS为1时,仅提供一个P(逻辑处理器核心数),所有的goroutine都在这一个P上执行。
three
one
two
goroutine会先填充本地执行队列(然后填充全局执行队列),所以按顺序执行。
之所以首先输出three,是因为go运行时调度时会会把最后一个goroutines直接关联P执行。
参考: