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模型

参考:http://topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/GMP%E5%8E%9F%E7%90%86%E4%B8%8E%E8%B0%83%E5%BA%A6.html

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执行。

 

参考:

1. http://mian.topgoer.com/

2. Golang精编100题-搞定golang面试

posted @ 2020-08-03 15:42  yuxi_o  阅读(343)  评论(0编辑  收藏  举报