Go语言躲坑经验总结

发表于 2年以前  | 总阅读数:731 次

导读 introduction

本文收集一些使用Go开发过程中非常容易踩坑的case,所有的case都有具体的代码示例,以及针对的代码修复方法,以避免大家再次踩坑。通常这些坑的特点就是代码正常能编译,但运行结果不及预期或是引入内存漏洞的风险。

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.60GHz
BenchmarkRangeHiPerformance-8            9767457              1255 ns/op
BenchmarkRangeLowPerformance-8               975          11513216 ns/op
PASS
ok      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 r ecover函数 只在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 main

var msg string
var done bool
func setup() {
    msg = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {
    }
    println(msg)
}

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

解决办法就是使用显示同步方案, 使用通道进行同步通信。


package main

var 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 main

import (
    "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

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/VS7DZg9qb_QmqAKqm_zc-Q

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237226次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8062次阅读
 目录