• Golang 新手可能踩的坑


    Java有经典的《Effective Java》,但是Google了很久也没有找到类似的golang书籍。可能golang本身足够简单,坑也比较少吧。总结了一些,自己觉得可能对golang新手有帮助的小TIPs。关于字符串的处理,这里没有列举,感兴趣的同学可以自己在网上找到相关资料。总的原则和Java等语言类似,如果有大量字符串处理的逻辑,最好不要直接使用+ 而是找一种有buffer的替代方式。

    defer

    defer 执行循序为LIFO(后进先出),参数的值在defer语句执行时就已经确定。

    1. for i := 0; i < 5; i++ {
    2. defer fmt.Printf("%d ", i)
    3. }

    输出

    4 3 2 1 0 

    defer 并不是免费的。比如 defer Mutex.Unlock 比直接Mutex.Unlock开销更大

    对于一些简短的加锁-修改某个值-释放锁的操作可以这么做。但是如果在加锁之后,可能在不同的分支都需要执行释放锁,还是建议直接使用defer Mutex.Unlock, 否则在(以后)修改或者新增分支的时候可能忘记释放锁从而引入bug。除非必要,应该以代码的可维护性为主

    1. var i int
    2. func unlockDefer(l *sync.Mutex) {
    3. l.Lock()
    4. defer l.Unlock()
    5. i = rand.Int()
    6. }
    7. func unlockDirect(l *sync.Mutex) {
    8. l.Lock()
    9. i = rand.Int()
    10. l.Unlock()
    11. }
    12. func BenchmarkUnlockDefer(b *testing.B) {
    13. l := &sync.Mutex{}
    14. for n := 0; n < b.N; n++ {
    15. unlockDefer(l)
    16. }
    17. }
    18. func BenchmarkUnlockDirect(b *testing.B) {
    19. l := &sync.Mutex{}
    20. for n := 0; n < b.N; n++ {
    21. unlockDirect(l)
    22. }
    23. }
    24. BenchmarkUnlockDefer-4 20000000 56.9 ns/op
    25. BenchmarkUnlockDirect-4 50000000 31.7 ns/op

    切片和数组

    array的长度是类型的一部分

    [3]int[4]int 是不同的类型

    array 是传值的,值指的是整个数组的内容

    1. func modify(a [3]int) {
    2. a[0] = 0
    3. }
    4. func main() {
    5. x := [...]int{3,2,1}
    6. modify(x)
    7. fmt.Println(x)
    8. }

    输出

    [3 2 1]

    slice 本质也是传值,但看起来像传递了底层数组的引用

    golang 都是传值的,slice看起来像是传引用是因为slice的值本身包含了一个对底层数组的引用(指针)。通过查看下面的slice结构体描述就很清晰。src/runtime/slice.go

    1. type slice struct {
    2. array unsafe.Pointer
    3. len int
    4. cap int
    5. }

    slice 不能作为map对象的key,因为slice没有完全实现==操作符,但是array可以。

    slice 只能使用== 判断是否为nil。

    slice 与内存模型

    append 不是线程安全的

    1. var s []int
    2. var wg sync.WaitGroup
    3. for i := 0; i < 10; i++ {
    4. wg.Add(1)
    5. go func(i int) {
    6. s = append(s, i)
    7. wg.Done()
    8. }(i)
    9. }
    10. wg.Wait()
    11. fmt.Println(s)

    一种可能的输出:

    [0 6 7 9]

    a[x]与b[y]是否存在竞态(“竞态”就是在多线程的编程中,你在同一段代码里输入了相同的条件,但是会输出不确定的结果的情况。),与a、b是否为同一slice无关,只与a[x]与b[y]是否指向同一块内存区域有关

    golang language specification中关于variables的介绍 中有这样一句话

    Structured variables of array, slice, and struct types have elements and fields that may be addressed individually. Each such element acts like a variable.
    译:数组、片和结构类型的结构化变量具有可以单独寻址的元素和字段。每个这样的元素就像一个变量。

    也就是说对于slice s而言,我们应该将s[i]看做是一个独立的变量(variable)。例如,对于下面这段代码,虽然a、b是两个完全不同是slice,但是a[1]与b[0]指向了同一块内存区域,等同于同一个变量。如果对a[1]与b[0]的并发访问不做可见性保护(如加锁),那么可能在代码中出现并发bug。(并不一定会触发这个bug,但是存在几率。因为现阶段groutine的加载速度没有main程序快,就和这个demo来说)

    1. func main() {
    2. a := []int{1,2}
    3. b := a[1:]
    4. go func() {
    5. a[1] = 0
    6. }()
    7. fmt.Println(b[0])
    8. }
    9. go run -race ./
    10. 2
    11. ==================
    12. WARNING: DATA RACE
    13. Write at 0x00c0000a2008 by goroutine 6:
    14. main.main.func1()
    15. /Users/vicxiao/workspace/go/test/main.go:11 +0x47
    16. Previous read at 0x00c0000a2008 by main goroutine:
    17. main.main()
    18. /Users/vicxiao/workspace/go/test/main.go:13 +0xaa
    19. Goroutine 6 (running) created at:
    20. main.main()
    21. /Users/vicxiao/workspace/go/test/main.go:10 +0x98
    22. ==================
    23. Found 1 data race(s)

    如果将第5行a[1]=0 替换为a[0]=0,那么因为a[0]与b[0]指向的是不同内存区域,就不会有竞态发生。

    与[]byte相关的性能问题

    slice 引用底层数组而造成内存泄露/GC压力

    例如,使用ioutil.ReadAll读取了一个10M的文件,并将其前512K切片为另外一个slice b使用。只要b还被引用,那么其指向的底层数组也不会被释放。

    复制数据时使用copy比append性能更优

    1. import (
    2. "crypto/rand"
    3. "testing"
    4. )
    5. var (
    6. src = make([]byte, 512)
    7. dst = make([]byte, 512)
    8. )
    9. func genSource() {
    10. rand.Read(src)
    11. }
    12. func BenchmarkCopy(b *testing.B) {
    13. for n := 0; n < b.N; n++ {
    14. b.StopTimer()
    15. genSource()
    16. b.StartTimer()
    17. copy(dst, src)
    18. }
    19. }
    20. func BenchmarkAppend(b *testing.B) {
    21. for n := 0; n < b.N; n++ {
    22. b.StopTimer()
    23. genSource()
    24. b.StartTimer()
    25. dst = append(dst, src...)
    26. }
    27. }

    注:

    dst作为全局变量是防止编译器优化for-loop

    测试时,机器不繁忙

    1. uptime;go version;go test -bench=. ./
    2. 11:56:10 up 294 days, 14:58, 3 users, load average: 0.58, 0.52, 0.63
    3. go version go1.14.1 linux/amd64
    4. goos: linux
    5. goarch: amd64
    6. pkg: copyvsappend
    7. BenchmarkCopy-40 9808320 116 ns/op
    8. BenchmarkAppend-40 479055 8740 ns/op
    9. PASS

    如果知道Reader里的数据大小,使用ReadFull而不是ioutil.ReadAll

    常见的场景如读取HTTP body。如果HTTP header中包含了Content-Length那么我们先申请足够大小的内存,然后调用一次io.ReadFull 是最佳的选择。如果我们使用ioutil.ReadAll 则会发生多次不必要的内存申请和内存拷贝。因为ReadAll 是以一个较小的buffer开始,不断地循环调用ReadFull.

    for range

    如果每个元素比较大,循环时使用range 取值的方式遍历,性能较差

    这个比较直观,因为有很多内存拷贝

    1. var X [1 << 15]struct {
    2. val int
    3. _ [4096]byte
    4. }
    5. var Result int
    6. func BenchmarkRangeIndex(b *testing.B) {
    7. var r int
    8. for n := 0; n < b.N; n++ {
    9. for i := range X {
    10. x := &X[i]
    11. r += x.val
    12. }
    13. }
    14. Result = r
    15. }
    16. func BenchmarkRangeValue(b *testing.B) {
    17. var r int
    18. for n := 0; n < b.N; n++ {
    19. for _, x := range X {
    20. r += x.val
    21. }
    22. }
    23. Result = r
    24. }
    25. func BenchmarkFor(b *testing.B) {
    26. var r int
    27. for n := 0; n < b.N; n++ {
    28. for i := 0; i < len(X); i++ {
    29. x := &X[i]
    30. r += x.val
    31. }
    32. }
    33. Result = r
    34. }
    35. BenchmarkRangeIndex-4 10000 127986 ns/op
    36. BenchmarkRangeValue-4 50 25939345 ns/op
    37. BenchmarkFor-4 10000 127817 ns/op

    这个有什么好的替代方法吗,比如直接使用for i 遍历数组长度?(从根本解决,要么不要创建大数组,要么大数组不要遍历)

    embedding

    如果类型S内嵌了T,如果T定义函数receiver都是*T类型,那么S定义的函数也应该以*S为receiver为佳

    这个主要是防止引入bug,例如下面的并发控制器,错误地在外层将receiver定义为值类型。

    1. type ConcurrentLocker struct{
    2. sync.Map
    3. }
    4. type LeaveFunc func()
    5. func (cl ConcurrentLocker) Enter(key string) (bool, LeaveFunc) {
    6. if _, occupied := cl.LoadOrStore(key, struct{}{}); !occupied {
    7. return true, func(){
    8. cl.Delete(key)
    9. }
    10. }
    11. return false, nil
    12. }
    13. func main() {
    14. cl := ConcurrentLocker{}
    15. key := "some-key"
    16. fmt.Println(cl.Enter(key))
    17. fmt.Println(cl.Enter(key))
    18. }

    这个错误的代码并不能达到并发控制的效果:

    1. $ go run ./
    2. true 0x10939e0
    3. true 0x10939e0

    当然修复的办法可以是内嵌* sync.Map, 但是这样初始化的时候不够优雅,需要单独初始化内部的map指针。个人更倾向于与将Enter 的接受者申明为* ConcurrentLocker, 这样既逻辑正确,其作为整体暴露的函数接受者也统一。

    GORM.DB的使用

    reflect: reflect.Value.SetLen using unaddressable value这也是新手极易容易犯得错误。

    如果出现这个错误

     那么就说明在我们使用GORM的时候,某些地方对指针的使用出现错误,也便需要我们去检查代码中是否有些地方缺少指针的使用。例如我的代码中:

    因上方datas已经被指针引用,而在使用GORM时,未使用指针的结构体,导致GORM在查询到数据赋值时出现错误,解决方式如下:

     

     

  • 相关阅读:
    09-流媒体-FLV解复用
    Python源码剖析3-列表对象PyListObject
    投稿玄学之SCI给了大修,还会拒稿吗?
    SpringBoot+Vue 的网上图书商城管理系统
    DHCP服务的八种报文(消息)作用
    google cloud DevOps Engineer
    电机调试说明SimpleFOC和ODrive
    百度交易中台之内容分润结算系统架构浅析
    【力扣每日一题】2023.9.10 课程表Ⅱ
    微信小程序——常用组件的属性介绍
  • 原文地址:https://blog.csdn.net/qq_55880012/article/details/127769931