mzh/blog

几个Go新手易犯的错误

最近跟新同事做code review,发现组里的小朋友犯了一些几个新手错误,想想自己以前也犯过这些错,分享给大家,希望大家看完以后可不要犯咯。

  1. 跨goroutine读写不加锁
  2. 文件打开后不关闭
  3. 滥用string方法
  4. channel死锁
  5. 不用合适的方法做事
  6. 文件整个读取进内存

跨goroutine读写不加锁

这个是最常见的错误,很多小朋友并没有并发编程的思想,对于同一个内存空间进行同时读写,例如大家可以猜猜这段代码的执行结果:

func main() {
    c := make(chan bool)
    m := make(map[string]string)
    go func() {
        m["1"] = "a" // 第一次冲突访问
        c <- true
    }()
    m["2"] = "b" // 第二次冲突访问
    <-c
    for k, v := range m {
        fmt.Println(k, v)
    }
}

代码出自Golang官方博客《Data Race Detector》,结果是,我也不知道!因为这是运行时(runtime)决定的,运气好时,ab都有,要是倒霉时,只有a或者b,这也就是为什么代码在这种地方需要加锁了(sync.Mutex),因为要保证同时有且只有一个goroutine能访问这个map对象。

同时推荐一本好书《七周七并发模型》,里面详细的描述了这种并发方式,锁模式,而Go推荐的CSP模式,再开一篇文章来讲了。

文件打开后不关闭

file, err := os.Open("file.go") // For read access.
if err != nil {
    log.Fatal(err)
}

很多小朋友写到这,就继续写业务其他的代码,殊不知在这里埋下了一个祸根,当打开的文件过多时,很有可能触发too many open files的系统错误。要知道,*nix系统中,一切皆文件,所以当你触发这个错误时,可能不仅仅是文件打开过多,而是整个程序陷入崩溃。那怎么改呢,很简单,Go官方还专门出了一个语句来帮助大家 —-就是defer

file, err := os.Open("file.go") // For read access.
if err != nil {
    log.Fatal(err)
}  
defer file.Close() //在这里添加defer语句

滥用string方法

如果要你拼接两个字符串,”foo”和”bar”,你会怎么做?常见的做法是:

a, b := "foo", "bar"  
c := a + b

既然这个做法自然是错的,可是为什么呢? 因为Go的字符对象是不可变的,意味着每一个新对象都是新的内存空间,当越来越多时,gc一定会不堪重负的。那map对象怎么办?map[[]byte]interface{}这样的结构编译器是不认的,Go官方给出了一个编译器级的优化

m := map[string]string  
b := []byte("hello")  
m[string(b)] = "world" //注意这个string方法,只能用在这里  
/*   
这么做一样会分配c的内存空间  
c := string(b)   
m[c] = "world"  
*/

channel死锁

r := make(chan int, 1)  
r <- 2  
r <- 3 // 代码似乎只执行到这了  
<-r

很多新手都在这里问:”go不是号称轻量级纤程么,怎么卡住呢?”,而且在Go 1.6 runtime会直接报出panic。那是因为这个channel已经固定数量了,如果channel内的数据不被消耗,那么这个负责输入的goroutine就会一直卡在r<-3那里,直到channel中的2被取走。

小结

前三个错误我工作中都已经碰到过4次以上,绝大多数都是编程新手犯的错误,不仅仅局限于Go,每次都要花半个小时给新人讲,这下可以丢文章给他们了。而剩下的两个我就不讲了,因为用atoi转化unix timestamp,不是用time库的人并不多……把整个文件读到buffer中,很容易就会发现内存超量了……

希望对大家有帮助,欢迎来@mengzhuo里面的提问题或者吐槽~