About Blog GitHub

26 Mar 2016
go常见错误总结

最近为了学习go语言,花了点时间翻译《the way to go》这本书相关章节:

详见:https://github.com/Unknwon/the-way-to-go_ZH_CN

在翻译过程中学习了一些go常见的错误和陷阱,特此总结一下,以便自己在今后使用go时少犯错误。

1 误用短声明:=导致变量覆盖

例如,下列代码中remember变量在if语句之外永远都是false,因为if语句中误用了短声明:=。重新定义了一个remember,自动覆盖外面的remember。所以在if语句中操作的remember变量和外面定义的remember不是同一个变量,导致remember在if语句之外一直都是false。

var remember bool = false
if something {
    remember := true
}

正确写法应该是:

var remember bool = false
if something {
    remember = true 
}

2 误用字符串操作导致大量的内存开销

go语言中字符串也是不可变的,比如当连接2个字符串:a+=b,尤其在一个循环中进行类似操作时,因为源字符串不可变,会导致大量的内存拷贝,造成内存开销过大。所以一般使用一个字符数组代替字符串,将字符串内容写入一个缓存中,代码如下:


var b bytes.Buffer

for condition {
    b.WriteString(str) // 将字符串str写入缓存buffer
}
    return b.String()

3 误用defer关闭文件

如果在一个for循环内部处理一系列文件,我们希望使用defer确保文件处理完毕后能自动被关闭。代码如下:


for_, file:=range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    defer f.Close()
    f.Process(data)
}

但是,defer在循环结束后没有被执行,所以文件一直没有被关闭。因为defer仅在函数返回时才能自动执行。所以正确的写法应该是:

for_, file:=range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    f.Process(data)
    f.Close()
 }

4 误用new和make

例如错误的使用new初始化一个map,错误使用make创建一个数组等。new和make的使用场景如下:

  • 切片、映射和通道,使用make

  • 数组、结构体和所有的值类型,使用new

5 误用指向切片的指针

在go语言中,切片实际是一个指向数组的指针。所以当我们需要将切片作为一个参数传递给函数时,实际就是传递了一个指针变量,并且在函数内部可以改变该变量,而不是传递一个值拷贝,所以当切片作为参数传递是,不需要解引用切片,即:

  • 正确的做法:

func findBiggest( listOfNumbers []int ) int {}

  • 错误的做法:

func findBiggest( listOfNumbers *[]int ) int {}

6 误用指针指向一个接口类型

例如以下代码,nexter是一个接口类型,并且定义了一个next()方法读取下一字节。函数nextFew将nexter接口作为参数并读取接下来的num个字节,并返回一个切片。但是nextFew2使用一个指向nexter接口类型的指针作为参数传递给函数,编译程序时,系统会给出一个编译错误:n.next undefined (type *nexter has no field or method next) 。所以切记不要使用一个指针指向接口类型。


package main

import (
    fmt
)

type nexter interface {
    next() byte
}

func nextFew1(n nexter, num int) []byte {
    var b []byte
    for i:=0; i < num; i++ {
        b[i] = n.next()
    }
    return b
}

func nextFew2(n *nexter, num int) []byte {
    var b []byte
    for i:=0; i < num; i++ {
        b[i] = n.next() // 编译错误:n.next未定义(*nexter类型没有next成员或next方法)
    }
    return b
}

func main() {
    fmt.Println(Hello World!)
}

7 误用指针传递值类型参数

当为一个自定义类型定义方法时,如果不想让该方法改变接受者的数据,那么接受者是一个值类型,传递的是一个值拷贝,这里看似造成了内存开销,但其实值类型的内存是在栈上分配的,分配速度快且开销不大。但是如果传递一个指针类型,go编译器在很多情况下会认为需要创建一个对象,并将对象存入堆中,导致额外的内存分配。所以,如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。

8 误用协程和通道

如果在一个循环内部使用了协程处理某些事务。当使用break、return或者panic跳出一个循环时,很有可能会导致内存溢出,因为此时协程正在处理某事务而被阻塞。因此在实际代码中,除非此处代码并发执行显得非常重要,才使用协程和通道,否则仅需写一个简单的过程式循环即可。

参考

《the way to go》



LEo at 23:11

About Blog GitHub