作者 | 百度小程序团队

导读
本文收集一些应用Go开发过程中非常容易踩坑的case,所有的case都有具体的代码示例,以及针对的代码修复办法,以防止大家再次踩坑。通常这些坑的特点就是代码失常能编译,但运行后果不迭预期或是引入内存破绽的危险。

全文7866字,预计浏览工夫20分钟。

01 参数传递误用

1.1 误对指针计算Sizeof

对任何指针进行unsafe.Sizeof计算,返回的后果都是 8 (64位平台下)。稍不留神就会引发谬误。

谬误示例:

func TestSizeofPtrBug(t *testing.T) {    type CodeLocation struct {        LineNo int64        ColNo  int64    }    cl := &CodeLocation{10, 20}    size := unsafe.Sizeof(cl)    fmt.Println(size) // always return 8 for point size}

倡议应用示例:独自编写一个只解决值大小的函数 ValueSizeof。

func TestSizeofPtrWithoutBug(t *testing.T) {    type CodeLocation struct {        LineNo int64        ColNo  int64    }    cl := &CodeLocation{10, 20}    size := ValueSizeof(cl)    fmt.Println(size) // 16}func ValueSizeof(v any) uintptr {    typ := reflect.TypeOf(v)    if typ.Kind() == reflect.Pointer {        return typ.Elem().Size()    }    return typ.Size()}

1.2 可变参数为any类型时,误传切片对象

当参数的可变参数是any类型时,传入切片对象时肯定要用开展形式。

    appendAnyF := func(t []any, toAppend ...any) []any {        ret := append(t, toAppend...)        return ret    }    emptySlice := []any{}    slice2 := []any{"hello", "world"}    // bug append slice as a element    emptySlice = appendAnyF(emptySlice, slice2)    fmt.Println(emptySlice) // only 1 element [[hello world]]    emptySlice = []any{}    emptySlice = appendAnyF(emptySlice, slice2...)    fmt.Println(emptySlice) // [hello world]

1.3 数组是值传递

数组在函数或办法中入参传递是值复制的形式,不能用入参的形式进函数或办法内批改数组内容进行返回的。

示例代码如下:

    arr := [3]int{0, 1, 2}    f := func(v [3]int) {        v[0] = 100    }    f(arr)           // no modify to arr    fmt.Println(arr) // [0 1 2]

1.4 切片扩容后会新申请内存,不再与内存援用有任何关联

这里坑在,如果从一个数组中引入一个切片,一旦这个切片引发扩容后,则与原来的援用内容没有任何关系。

    arr := []int{0, 1, 2}    f := func(v []int) {        v[0] = 100// can modify origin array        v = append(v, 4) // new memory allocated        v[0] = 50// no modify to origin array    }    f(arr)    fmt.Println(arr) // [100 1 2]

下面的示例代码,扩容切片前对内容的批改能够影响到arr数组,阐明是共享内存地址援用的,一旦扩容后,则是从新申请了内存,与数组不再是一个内存援用了。

1.5 返回参数尽量避免应用共享数据的切片对象,容易导致原始数据净化

这种场景就是如果通过函数返回值形式从一个大数组获取局部外部,尽量不要用切片共享的形式,能够应用copy的形式来替换。

上面的代码,通过ReadUnsafe读取切片后,批改内容同步影响原始的内容。

type Queue struct {    content []byte    pos     int}func (q *Queue) ReadUnsafe(size int) []byte {    if q.pos+size >= len(q.content) {        return nil    }    pos := q.pos    q.pos = q.pos + size    return q.content[pos:q.pos]}func TestReadUnsafe(t *testing.T) {    c := [200]byte{}    q := &Queue{content: c[:]}    v := q.ReadUnsafe(10)    v[0] = 1    fmt.Println(q.content[0]) // 1  q.content值曾经被批改}

正确的批改如下,应用copy创立一份新内存:

func (q *Queue) ReadSafe(size int) []byte {    if q.pos+size >= len(q.content) {        return nil    }    pos := q.pos    q.pos = q.pos + size    ret := make([]byte, size)    copy(ret, q.content[pos:q.pos])    return ret}func TestReadSafe(t *testing.T) {    c := [200]byte{}    q := &Queue{content: c[:]}    v := q.ReadSafe(10)    v[0] = 1    fmt.Println(q.content[0]) // 0  q.content值平安}

02 指针相干应用的坑

2.1 误保留uintptr值

uintptr保留的以后地址的一个整型值,它一旦被获取后,是不会被编译器感知的,也就是它就是一个一般变量,不会追溯内存实在地址变动。

    slice := []int{0, 1, 2}    ptr := unsafe.Pointer(&slice[0]) // get array element:0 pointer    slice = append(slice, 3) // allocate new memory    ptr2 := unsafe.Pointer(&slice[0])    // ptr is 824633770392, ptr2 is 824633762896, ptr==ptr2 result is false    fmt.Println(fmt.Sprintf("ptr is %d, ptr2 is %d, ptr==ptr2 result is %v", ptr, ptr2, ptr == ptr2))

2.2 len与cap 对空指针nil与空值返回雷同

针对切片, 用len与cap操作时,空值与nil都是返回0, 针对map, 用len操作时,空值与nil都是返回0。

     var slice []int = nil    fmt.Println(len(slice), cap(slice)) // 0 0    var slice2 []int = []int{}    fmt.Println(len(slice2), cap(slice2)) // 0 0    var mp map[int]int = nil    fmt.Println(len(mp)) // 0    var mp2 map[int]int = map[int]int{}    fmt.Println(len(mp2)) // 0

2.3 用new对map类型进行初始化

用new对map进行创立,编译器不会报错,然而无奈对map进行赋值操作的。正确应应用make进行内存调配。

        mp := new(map[int]int)        f := func(m map[int]int) {            m[10] = 10        }        f(*mp) // assignment to entry in nil map

2.4 空指针和空接口不等价

对于接口类型是能够用nil赋值的,但如果对于接口指针类型,其值对应的并不一个空接口。Go语言编译器仿佛在这个解决,会非凡解决。

// MyErr just for demotype MyErr struct{}func (e *MyErr) Error() string {    return""}func TestInterfacePointBug(t *testing.T) {    var e *MyErr = nil    var e2 error = e // e2 will never be nil.    fmt.Println(e2 == nil)}

03 函数,办法与控制流相干

3.1 循环中应用闭包谬误援用同一个变量

起因剖析:闭包捕捉内部变量,它不关怀这些捕捉的变量或常量是否超出作用域,只有闭包在应用,这些变量就会始终存在。

  type S struct {        A string        B string        C string    }    typ := reflect.TypeOf(S{})    funcArr := make([]func() string, typ.NumField())    for i := 0; i < typ.NumField(); i++ {        f := func() string {            return typ.Field(i).Name        }        funcArr[i] = f    }    fmt.Println(funcArr[0]()) // error reflect: Field index out of bounds

所以下面的示例代码,在循环中闭包函数只记录了i变量的应用,当循环完结后,i值变成了3。当调用该匿名函数时,就会援用i=3的值 ,呈现越界的异样。

正确处理的形式如下,只须要闭包前解决一下把i变量赋值给一个新变量。

  type S struct {        A string        B string        C string    }    typ := reflect.TypeOf(S{})    funcArr := make([]func() string, typ.NumField())    for i := 0; i < typ.NumField(); i++ {        index := i // assign to a new variable        f := func() string {            name := typ.Field(index).Name            return name        }        funcArr[i] = f    }    fmt.Println(funcArr[0]()) // A

3.2 元素内容较大时,不要用range遍历

用range来操作遍历应用上十分不便,然而它的遍历中是须要进行值赋值操作,遇到元素占用的内存比拟大时,性能就会影响较大。

上面是针对两种形式做了一下基准测试。

func CreateABigSlice(count int) [][4096]int {    ret := make([][4096]int, count)    for i := 0; i < count; i++ {        ret[i] = [4096]int{}    }    return ret}func BenchmarkRangeHiPerformance(b *testing.B) {    v := CreateABigSlice(1 << 12)    for i := 0; i < b.N; i++ {        len := len(v)        var tmp [4096]int        for k := 0; k < len; k++ {            tmp = v[k]        }        _ = tmp    }}func BenchmarkRangeLowPerformance(b *testing.B) {    v := CreateABigSlice(1 << 12)    for i := 0; i < b.N; i++ {        var tmp [4096]int        for _, e := range v {            tmp = e        }        _ = tmp    }}

测试后果如下:range形式的性能较for形式相差了近10000倍。

cpu: 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHzBenchmarkRangeHiPerformance-8            9767457              1255 ns/opBenchmarkRangeLowPerformance-8               975          11513216 ns/opPASSok      withoutbug/avoidtofix   26.270s

3.3 循环内调用defer造成销毁解决提早

在很多场景,在循环内申请资源在循环实现后开释,然而应用defer语句解决,是须要在以后函数退出时才会执行,在循环中是不会触发的,导致资源提早开释。

func main() {    for i := 0; i < 5; i++ {        f, err := os.Open("./mygo.go")        if err != nil {            log.Fatal(err)        }        defer f.Close()    }}

比拟好的解决办法就是在for循环里不要应用defer,间接进行销毁解决。

func main() {    for i := 0; i < 5; i++ {        f, err := os.Open("/path/to/file")        if err != nil {            log.Fatal(err)        }        f.Close()    }}

3.4 Goroutine无奈阻止主过程退出

后盾Goroutine无奈保障在办法退出来执行实现。

func main() {     gofunc() {        time.Sleep(time.Second)        fmt.Println("run")    }()   }

3.5 Goroutine 抛panic会导致过程退出

后盾Goroutine执行中,如果抛panic并不进行recover解决,会导致主过程退出。

上面的代码示例:

func main1() {    go func() {        panic("oh...")    }()    for i := 0; i < 3; i++ {        fmt.Println(i)        time.Sleep(time.Second)    }    fmt.Println("bye bye!")}

修改代码如下:

func main2() {    go func() {        defer func() {            recover() // should do some thing here        }()        panic("oh...")    }()    for i := 0; i < 3; i++ {        fmt.Println(i)        time.Sleep(time.Second)    }    fmt.Println("bye bye!")}

3.6 recover函数 只在defer函数内失效

须要留神:在非defer函数内,调用recover函数,是不会有任何的执行,也无奈来解决panic谬误。

上面的示例代码,是无奈解决panic的谬误:

func NoTestDeferBug(t *testing.T) {    recover()    panic(1) // could not catch}func NoTestDeferBug2(t *testing.T) {    defer recover()    panic(1) // could not catch}

正确的代码如下:

func TestDeferFixed(t *testing.T) {    defer func() {        recover()    }()    panic("this is panic info") // could not catch}

04 并发与内存同步相干

4.1 跨Goroutine之间不反对程序一致性内存模型

在Go语言的内存模型设计中, 内存写入程序性只能保障在繁多Goroutine内统一,跨Goroutine之间无奈保障监测变量操作程序的一致性。

上面是官网的例子:

package mainvar msg stringvar done boolfunc setup() {    msg = "hello, world"    done = true}func main() {    go setup()    for !done {    }    println(msg)}

下面代码的问题是,不能保障在 main 中对 done 的写入的监测时, 会对变量a的写入也进行监测,因而该程序也可能会打印出一个空字符串。更糟的是,因为在两个线程之间没有同步事件,因而无奈保障对 done 的写入总能被 main 监测到。main 中的循环不保障肯定能完结。

解决办法就是应用显示同步计划, 应用通道进行同步通信。

package mainvar msg string var done = make(chan bool)func setup() {    msg = "hello, world"    done <- true}func main() {    go setup()    <-done    println(msg)}

这样就能够保障代码执行过程中必然输入 hello,world。

更多内存同步浏览资料:https://go-zh.org/ref/mem

05 序列化相干

5.1 基于指针参数形式传递的反序列性能,都不会初始化要反序列化的对象字段

该问题常常产生的起因是基于指针参数形式传递的反序列函数其实做的只是值笼罩的性能,并不会把要反序化的对象的所有值进行初始化操作,这样就会导致未笼罩的值的保留. 像 json.Unmarshal, xml.Unmarshal 函数等。

上面是基于json对map 类型的变量进行json.Unmarshal的问题示例:

package mainimport (    "encoding/json"    "fmt")func main() {    val := map[string]int{}    s1 := `{"k1":1, "k2":2, "k3":3}`    s2 := `{"k1":11, "k2":22, "k4":44}`    json.Unmarshal([]byte(s1), &val)    fmt.Println(s1, val)    json.Unmarshal([]byte(s2), &val)    fmt.Println(s2, val)}

输入:

{"k1":1, "k2":2, "k3":3} map[k1:1 k2:2 k3:3]{"k1":11, "k2":22, "k4":44} map[k1:11 k2:22 k3:3 k4:44]

因为 json.UnMarshal 办法只会新增和笼罩 map 中的 key,不会删除 key。尽管第二个json字符串中没有k3的内容,但输入后果中仍然保留在了k3的内容。

要解决这个问题,每次 unmarshal 之前都从新申明变量即可。

06 其它杂项

6.1 数字类型转换越界陷阱

Go语言中,任何操作符不会扭转变量类型,上面示例引入一个坑, 呈现位移越界。

func TestOverFlowBug(t *testing.T) {    var num int16 = 5000    var result int64 = int64(num << 9)    fmt.Println(result) // 4096 overflow}

修改形式如下,须要操作前对类型转换:

func TestOverFlowFixed(t *testing.T) {    var num int16 = 5000    var result int64 = int64(num) << 9    fmt.Println(result) // 2560000}

6.2 map遍历是程序不固定

map的实现是通hash表进行分桶定位,同时map的遍历引入了随机实现,所以每次遍历的程序都可能变动。

    mp := map[int]int{}    for i := 0; i < 20; i++ {        mp[i] = i    }    for k, v := range mp {        fmt.Println(k, v)    }

——END——

参考资料:
[1]Effective Go 英文版:
https://go.dev/doc/effective_go
[2]Go 语言代码格调领导:
https://github.com/golang/go/...

举荐浏览:
PaddleBox:百度基于GPU的超大规模离散DNN模型训练解决方案
聊聊机器如何"写"好广告文案?
百度工程师教你玩转设计模式(适配器模式)
百度搜寻业务交付无人值守实际与摸索
分布式ID生成服务的技术原理和我的项目实战
揭秘百度智能测试在测试评估畛域实际