Go语言常见坑

这里列举的Go语言常见坑都是符合Go语言语法的, 可以正常的编译, 但是可能是运行结果错误, 或者是有资源泄漏的风险。

数组是值传递

在函数调用参数中, 数组是值传递, 无法通过修改数组类型的参数返回结果。

package main

import "fmt"

func main() {
  x := [3]int{1, 2, 3}

  func(arr [3]int) {
    arr[0] = 7
    fmt.Println(arr)
  }(x)

  fmt.Println(x)
}

运行程序,结果如下:

[7 2 3]
[1 2 3]

Process finished with exit code 0

在go中最好使用切片,因为切片是引用传递。这个和java是有区别的,java如下:

import java.util.Arrays;

public class Hello {
  public static void main(String[] args) {
    String[] arr = {"1","2","3"};
    test(arr);
    System.out.println("main arr is " + Arrays.asList(arr));
  }
  
  public static void test(String[] arr){
    arr[0] = "10";
    System.out.println("test arr is " + Arrays.asList(arr));
  }
}

运行结果如下:

test arr is [10, 2, 3]
main arr is [10, 2, 3]

map遍历是顺序不固定

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

package main

func main() {
  m := map[string]string{
    "1": "1",
    "2": "2",
    "3": "3",
  }

  for k, v := range m {
    println(k, v)
  }
  println("--------------------------------------------")
  for k, v := range m {
    println(k, v)
  }
}

输出结果如下:

2 2
3 3
1 1
--------------------------------------------
1 1
2 2
3 3

Process finished with exit code 0

返回值被屏蔽

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

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

recover必须在defer函数中运行

  • recover捕获的是祖父级调用时的异常, 直接调用时无效:
func main() {  
  recover()
  panic(1)
}
  • 直接defer调用也是无效:
func main() {  
  defer recover()
  panic(1)
}
  • defer调用时多层嵌套依然无效:
func main() {
  defer func() {
    func() {
      recover()
    }()
  }()
  panic(1)
}
  • 必须在defer函数中直接调用才有效:
package main

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

main函数提前退出

后台Goroutine无法保证完成任务,这里hello不会打印出来:

func main() {
  go println("hello")
}

通过Sleep来回避并发中的问题

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

func main() {
  go println("hello")
  time.Sleep(time.Second)
}

类似的还有通过插入调度语句,当字符串长度很长时,也不会输出。

func main() {
  go println("hello")
  runtime.Gosched()
}
    }
  }()

  for {
    runtime.Gosched()
  }
}

Goroutine间不满足顺序一致性内存模型

因为在不同的Goroutine, main函数可能无法观测到done的状态变化, 那么for循环会陷入死循环:

package main

import (
  "runtime"
)
var msg string
var done bool = false

func main() {
  runtime.GOMAXPROCS(1)

  go func() {
    msg = "hello, world"
    done = true
  }()

  for {
    if done {
      println(msg)
      break
    }
  }
}

解决的办法是用显式同步:

package main

import (
  "runtime"
)

var msg string
var done = make(chan bool)

func main() {
  runtime.GOMAXPROCS(1)

  go func() {
    msg = "hello, world"
    done <- true
  }()

  <-done
  println(msg)
}

闭包错误引用同一个变量

如下代码,最后只会输出五次5:

package main

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

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

package main

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

或者是通过函数参数传入:

package main

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

在循环内部执行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()
    }()
  }
}

切片会导致整个底层数组被锁定

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

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
}

空指针和空接口不等价

比如返回了一个错误指针, 但并不是空的error接口,昨天的分享中说到,接口作为两个元素实现:一个类型和一个值,所以这里返回的值{*MyError,p}

func returnsError() error {
  var p *MyError = nil
  if bad() {
    p = ErrBad
  }
  return p // Will always return a non-nil error.
}

内存地址会变化

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

func main() {
  var x int = 42
  var p uintptr = uintptr(unsafe.Poiner(&x))

  runtime.GC()
  var px *int = (*int)(unsafe.Poiner(p))
  println(*px)
}

当内存发送变化的时候, 相关的指针会同步更新, 但是非指针类型的uintptr不会做同步更新.

同理, cgo中也不能保存Go对象地址.

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的泄漏。

原文发布于微信公众号 - 我的小碗汤(mysmallsoup)

原文发表时间:2018-06-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户画像

H5中的标记方法

要使用H5标记,必须先进行如下的doctype声明,不区分大小写。Web浏览器通过判断文件开头有没有这个声明,来判断解析器和渲染类型是否切换到对应的H5模式。

821
来自专栏技术沉淀

Python: set实例透析

1122
来自专栏Nian糕的私人厨房

JavaScript 常见面试题分析(二)

④ call() 方法 apply() 方法 bind() 方法 (this 指向第一个参数)

993
来自专栏程序员互动联盟

【编程基础】C++ Primer快速入门三:两种控制语句

语句总是顺序执行的:第一条语句执行完了接着是第二条,第三条等等。这是最简单的情况,为了更好的控制语句的运行,程序设计语言提供了多种控制结构支持更为复杂的语句执行...

3439
来自专栏Golang语言社区

转-Go语言开发常见陷阱,你遇到过几个?

Go作为一种简便灵巧的语言,深受开发者的喜爱。但对于初学者来说,要想轻松驾驭它,还得做好细节学习工作。 初学者应该注意的地方: 大括号不能独立成行。 未使用变量...

3559
来自专栏xiaoxi666的专栏

c++ 继承类强制转换时的虚函数表工作原理

本文通过简单例子说明子类之间发生强制转换时虚函数如何调用,旨在对c++继承中的虚函数表的作用机制有更深入的理解。

2063
来自专栏程序员的SOD蜜

C#中?与??的区别

起初我也不知道C#中有??操作符,今天张鹏在查看我的MVC示例程序的时候问了这个问题,检查代码后发现,下面的代码是VS2010在生成MVC应用程序自己添加的: ...

2377
来自专栏用户2442861的专栏

Python基础学习笔记之(二)(华工大神)

         Python中每一个.py脚本定义一个模块,所以我们可以在一个.py脚本中定义一个实现某个功能的函数或者脚本,这样其他的.py脚本就可以调用...

1234
来自专栏编程心路

想学习php的,不如来这里看看

win+R打开命令行,cmd进DOS窗口 DOS命令开启关闭Apache和Mysql Apache启动关闭命令

1263
来自专栏我的博客

PHP中处理html相关函数集锦

1、html_entity_decode() 函数把 HTML 实体转换为字符。 Html_entity_decode() 是 htmlentities() ...

3546

扫码关注云+社区

领取腾讯云代金券