golang traps and pitfalls


迭代、作用域、lazy call

考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。以下代码值得我们好好讨论:

  var rmdirs []func()
  for _, d := range tempDirs(){
    dir := d // NOTE: necessary
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func(){
        os.RemoveAll(dir)
    })
  }

  // Do sometings ...

  // remove dirs
  for _, rmdir := range rmdirs{
    rmdir()    
  }

  

你可能感到很困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量。 如果删除那句看似很多余的dir := d局部变量创建赋值语句则程序是错误的。 在上面的程序中,for循环引入了新的词法块,循环变量dir在这个词法块中被 声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意的是, 函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir 为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已经完成, dir中存储的值等于最后一次迭代的值,这意味着,每次对os.RemoveAll的调用 删除的都是相同的目录。

通常,为了解决这类问题,我们会引入一个与循环变量同名的局部变量,作为循环 变量的副本。这虽然看起来很奇怪,但却很有用:

  for _, dir := range tempDirs(){
      dir := dir // declares inner dir, initialized to outer dir
  }
  

这个问题不仅局限在range循环,下面这段存在同样问题:

  var rmdirs []func()
  dirs := tempDirs()
  for i:=0; i<len(dirs); i++{
      os.MkdirAll(dirs[i], 0755) // OK
      rmdirs = append(rmdirs, func(){
          os.RemoveAll(dirs[i]) // FIXEME: incorrect!!!
    })
  }
  

类似地,以下代码也存在同样问题:

  m := make(map[string]* student)
  stus := []student{
      {Name: "zhao", Age: 20},
      {Name: "qian", Age: 21},
      {Name: "sun", Age: 22},
      {Name: "li", Age: 23},
    }

  for _, stu := range stus{
      // stu := stus[i]
      m[stu.Name] = &stu
  }

  return m
  

for 遇到 defer

在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些延迟函数才会执行。 下面的代码会导致系统的文件描述符耗尽: 因为在所有文件都被处理之前,没有文件会被关闭。

    for _, filename := range filenames{
        f, err := os.Open(filename)
        if err != nil{
            return err
        }
        defer f.Close() // NOTE: risky; could run out of the file descriptors

        // do something
    }
  

一种解决方法是将循环体中的defer语句移至另一个函数。每次循环时,调用这个函数。

    for _, filename := range filenames{
        if err := doFile(filename); err != nil{
            return err    
        }
    }

    func doFile(filename string) error{
        f, err := os.Open(filename)
        if err != nil{
            return err
        }
        defer f.Close()
        // ...
    }
  

*p++

p++ 在c/c++中,++运算符的优先级高于*, 因此先执行++,指针地址偏移所指数据类型个字节位置,由于后缀++的先使用后生效特点, 整体表现为先取p位置数据,然后指针+1。 而在go中,p++只对p所指地址的数据进行++运算。

个人以为,go采取这样的行为一是为了使语法更加简单而且明确; 二是go包含垃圾回收,如果++运算符作用到p会给GC的实现造成复杂

    func incr(p *int) int{
        *p++ // NOTE: 只是增加p指向的变量的值,并不改变p指针
        return *p
    }

    v := 1
    incr(&v)    // sizd effect: v is now 2
    fmt.Println(incr(&v)) // print 3
  

defer

命名返回参数允许defer延迟调用通过闭包读取和修改:

    func add(x, y int) (z int){
        defer func(){
            z += 100    
        }()

        z = x+y
        return
    }

    func main(){
        println(add(1,2)) // print: 103    
    }
  

当显式返回时,会先修改命名返回参数。

    func add(x, y int) (z int){
        defer func(){
            println(z) // 输出203
        }()

        z = x+y
        return z + 200 // 执行顺序: (z = z+200) -> (call defer) -> (ret)
    }

    func main(){
        println(add(1,2)) // print: 203    
    }
  

滥用defer可能会导致性能问题,尤其是在一个大循环中

      var lock sync.Mutex

      func test(){
          lock.Lock()
          lock.Unlock()
      }

      func test_defer(){
        lock.Lock()
        defer lock.Unlock()
      }

    func BenchmarkTest(b *testing.B){
        for i:=0; i<b.N; i++{
            test()    
        }
    }

    func BenchmarkTestDefer(b *testing.B){
        for i:=0; i<b.N; i++{
            test_defer()    
        }    
    }

  
1
2
3
4
> result:

BenchmarkTest 50000000 43 ns/op
BenchmarkTestDefer 20000000 128 ns/op

defer , panic

    func test(){
        defer func(){
            fmt.Println(recover())
        }()

        defer func(){
            panic("defer panic")     
        }()

        panic("test panic")
    }

    func main(){
        test()    
    }
  

捕获函数recover只有在延迟调用内直接调用才会终止错误,否则总是返回nil。 任何未捕获的错误都会沿调用堆栈向外传递.

    func test(){
        defer recover() // 无效
        defer fmt.Println()
    }
  

Map

从Map中取回的是一个value临时复制品,对其成员的修改是没有任何意义的。

  type user struct{ name string }

  m := map[int]user{ // 当map因扩张而重新哈希时,各键值项存储位置都会发生改变。
    1: {"user1"},    // 因此,map被设计成not addressable. 类似m[1].name这种期望
  }                  // 透过原value指针修改成员的行为自然会被禁止。

  m[1].name = "Tom"  // Error: cannot assign to m[1].name

  

正确的做法是完整替换value或使用指针。

  u := m[1]
  u.name = "Tom"
  m[1] = u // 替换 value

  m2 := map[int]*user{
    1: &user{"user1"},    
  }
  m2[1].name = "Jack"
  

可以在迭代时安全删除键值。但如果期间有新增操作,那就不知道会有什么意外了。

  for i:=0; i<5; i++{
    m := map[int]string{
        0: "a", 1: "a", 2: "a", 3: "a", 4: "a",    
        5: "a", 6: "a", 7: "a", 8: "a", 9: "a",    
    }    

    for k := range m{
        m[k+k] = "x"
        delete(m, k)
    }

    fmt.Println(m)
  }