Go语言一些容易踩坑的地方

1、可变参数是空接口类型
func main() {
     var a = []interface{}{1, 2, 3}
      fmt.Println(a)
      fmt.Println(a...)
}

不管是否展开,编译器都无法发现错误,但是输出是不同的:

[1 2 3]
1 2 3
2、数组是值传递

在函数调用参数中,数组是值传递,无法通过修改数组类型的参数返回结果,因此必要时使用切片

3、切片的函数传参

Go语言中,如果以切片为参数调用函数时,有时候会给人一种参数采用了传引用的方式的假象:因为在被调用函数内部可以修改传入的切片的元素。其实,任何可以通过函数参数修改调用参数的情形,都是因为函数参数中显式或隐式传入了指针参数。函数参数传值的规范更准确说是只针对数据结构中固定的部分传值,例如字符串或切片对应结构体中的指针和字符串长度结构体传值,但是并不包含指针间接指 向的内容。将切片类型的参数替换为类似 reflect.SliceHeader 结构体就很好理解切片传值的含义了:

func twice(x []int) {
    for i := range x {
        x[i] *= 2
    }
}

type IntSliceHeader struct {
    Data []int
    Len  int
    Cap  int
}

func twice(x IntSliceHeader) {
    for i := 0; i < x.Len; i++ {
        x.Data[i] *= 2
    }
}

如果只是修改切片中的元素值,那么函数参数直接传切片即可,但如果要修改切片的长度,要增加或者减少切片中的成员,则需要传入切片的指针或者通过返回值返回修改后的切片

func modifySlice(array *[]int, elem int) {
    *array = append(*array, elem)
}

func modifySlice(array []int, elem int) []int {
    array = append(array, elem)
    return array
}

因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。除了数据之外,切片结构还包含了切片长度和切片容量信息,这2个信息也是传值的。如果被调用函数中修改了 Len 或 Cap 信息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切片。这也是为何内置的 append 必须要返回一个切片的原因

4、map遍历顺序不固定

map是一种hash表的实现,每次遍历的顺序可能都不一样

5、返回值被屏蔽

在局部作用域中,命名的返回值会被同名的局部变量屏蔽

func Foo() (err error) {
    if err := Bar(); err != nil {
         return
      }
       return
}

6、recover必须在defer函数中运行

recover捕获的是祖父级调用时的异常,直接调用时无效:

func main() {
      recover()
      panic(1)
}

直接defer调用也是无效的:

func main() {
     defer recover()
     panic(1)
}

defer调用时多层嵌套依然无效:

func main() {
     defer func() {
         func() { recover() }()
     }()
     panic(1)
}
7、main函数提前退出

后台goroutine无法保证完成任务

func main() {
     go println("hello")
}
8、通过sleep来回避并发中的问题

休眠并不能保证输出完整的字符串

func main() {
     go println("hello")
     time.Sleep(time.Second)
}
9、独占CPU导致其他goroutine饿死

goroutine是协作式抢占调度,不会主动放弃CPU

func main() {
     runtime.GOMAXPROCS(1)
     go func() {
         for i := 0; i < 10; i++ {
               fmt.Println(i)
          }
      }()
     for {} // 占用CPU
}

解决的方法是在for循环中加入runtime.Gosched()调度函数

func main() {
     runtime.GOMAXPROCS(1)
     go func() {
         for i := 0; i < 10; i++ {
               fmt.Println(i)
          }
      }()
     for {
           runtime.Gosched()
      } // 占用CPU
}

或者是通过阻塞方式避免cpu占用

func main() {
     runtime.GOMAXPROCS(1)
     go func() {
           for i := 0; i < 10; i++ {
                fmt.Println(i)
            }
            os.Exit(0)
      }()
      select{}
}
10、不同Goroutine之间不满足顺序一致性内存模型

因为在不同的Goroutine中,main函数无法保证能打印出hello,world:

var msg string
var done bool
func setup() {
      msg = "hello, world"
      done = true
}
func main() {
      go setup()
     for !done {
      }
      println(msg)
}

解决的办法是显示同步:

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

msg的写入是在channel发送之前,所以能保证打印hello,world

11、闭包错误引用同一个变量

这个很容易踩中

func main() {
     for i := 0; i < 5; i++ {
         defer func() {
             println(i)
          }()
      }
}

改进的方法是在每次迭代中生成一个局部变量

func main() {
     for i := 0; i < 5; i++ {
         tmp := i
         defer func() {
             println(tmp)
          }() //或者通过函数参数传入
      }
}
12、在循环内部执行defer语句

defer在函数退出时才能执行,在for执行defer会导致资源延迟释放:

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

解决的方法可以在for中构造一个局部函数,在局部函数内部执行defer:

func main() {
     for i := 0; i < 5; i++ {
          func(){
              f, err := os.Open("/path/to/file")
              if err != nil {
                   log.Fatal(err)
               }
               defer f.Close()
          }
      }
}
13、切片会导致整个底层数组被锁定

切片会导致整个底层数组被锁定,底层数组无法释放内存。如果底层数组较大会对 内存产生很大的压力。

func main() {
    headerMap := make(map[string][]byte)
    for i := 0; i < 5; i++ {
         name := "/path/to/file"
         data, err := ioutil.ReadFile(name)
         if err != nil {
             log.Fatal(err)
         }
         headerMap[name] = data[:1]
       }
       // do some thing
}

解决的方法是将结果克隆一份,这样可以释放底层的数组:

func main() {
      headerMap := make(map[string][]byte)
      for i := 0; i < 5; i++ {
            name := "/path/to/file"
            data, err := ioutil.ReadFile(name)
            if err != nil {
                log.Fatal(err)
             }
             headerMap[name] = append([]byte{}, data[:1]...)
        }
       // do some thing
}
14、空指针和空接口不等价

比如返回了一个错误指针,但是并不是空的error接口

func returnsError() error {
     var p *MyError = nil
     if bad() {
         p = ErrBad
      }
      return p // Will always return a non-nil error.
}
15、内存地址会变化

Go语言中对象的地址可能发生变化,因此指针不能从其它非指针类型的值生成:

func main() {
    var x int = 42
    var p uintptr = uintptr(unsafe.Pointer(&x))
    runtime.GC()
    var px *int = (*int)(unsafe.Pointer(p))
    println(*px)
}

当内存发送变化的时候,相关的指针会同步更新,但是非指针类型的uintptr不会做 同步更新。
同理CGO中也不能保存Go对象地址。

16、Goroutine泄露

Go语言自带垃圾回收,因此内存一般不会泄露。但是Goroutine却存在泄露的情况,导致Goroutine引用的内存同样无法被回收

func main() {
    ch := func() <-chan int {
          ch := make(chan int)
          go func() {
               for i := 0; ; i++ {
                    ch <- i
               }
         } ()
        return ch
  }()
 for v := range ch {
       fmt.Println(v)
       if v == 5 {
            break
       }
   }
}

上面的程序中后台Goroutine向管道输入自然数序列,main函数中输出序列。但是 当break跳出for循环的时候,后台Goroutine就处于无法被回收的状态了。 我们可以通过context包来避免这个问题:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    ch := func(ctx context.Context) <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                select {
                case <-ctx.Done():
                    return
                case ch <- i:
                }
            }
        }()
        return ch
    }(ctx)
    for v := range ch {
        fmt.Println(v)
        if v == 5 {
            cancel()
            break
        }
    }
}

当main函数在break跳出循环时,通过调用 cancel() 来通知后台Goroutine退出,这样就避免了Goroutine的泄漏

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值