go中的5个常见错误(附代码)

304 阅读4分钟

Bug-risks是代码中的问题,会在生产中引起错误和破坏。漏洞是代码中的一个缺陷,会产生不想要的或不正确的结果。由于不良的编码实践,缺乏版本控制的最佳实践,需求的误传,以及不现实的开发时间安排,代码经常会出现bug-risks。在这篇文章中,让我们来看看到目前为止我们所看到的围棋中几个常见的bug-风险。

1.无限的递归调用

递归调用自身的函数需要有一个退出条件。否则,它就会永远地递归,直到系统的内存耗尽。

这个问题可能是由常见的错误引起的,比如忘记添加退出条件。它也可能 "故意 "发生。一些语言有尾部调用优化,这使得某些无限递归的调用可以安全使用。尾部调用优化允许你避免为一个函数分配一个新的堆栈框架,因为调用函数将返回它从被调用函数得到的值。最常见的用途是尾部递归,为利用尾部调用优化而编写的递归函数可以使用恒定的堆栈空间。然而,Go并没有实现尾部调用优化,你最终会耗尽内存。然而,这个问题并不适用于催生新的goroutine。

2.赋值到nil map

在添加任何元素之前,需要用make 函数(或map literal)来初始化一个map。一个新的、空的地图值是使用内置函数make ,它以map 类型和一个可选的容量提示作为参数:

make(map[string]int)
make(map[string]int, 100)

初始容量并不约束其大小:地图的增长是为了适应其中存储的项目数量,但nil 地图除外。一个nil 地图等同于一个空地图,只是不能添加任何元素。

不好的模式:

var countedData map[string][]ChartElement

好的模式:

countedData := make(map[string][]ChartElement)

3.方法修改了接收器

一个修改非指针接收器值的方法可能会产生不必要的后果。这是一个错误的风险,因为该方法可能会改变方法内部的接收器的值,但它不会反映在原始值中。为了传播这种变化,接收器必须是一个指针。

比如说:

type data struct {
	num   int
	key   *string
	items map[string]bool
}

func (d data) vmethod() {
	d.num = 8
}

func (d data) run() {
	d.vmethod()
	fmt.Printf("%+v", d) // Output: {num:1 key:0xc0000961e0 items:map[1:true]}
}

如果num 必须被修改:

type data struct {
	num   int
	key   *string
	items map[string]bool
}

func (d *data) vmethod() {
	d.num = 8
}

func (d *data) run() {
	d.vmethod()
	fmt.Printf("%+v", d) // Output: &{num:8 key:0xc00010a040 items:map[1:true]}
}

4.可能不需要的值被用于goroutine中

循环中的范围变量在每次迭代时都会被重复使用;因此,在循环中创建的goroutine将从上层范围指向范围变量。这样一来,goroutine就可以用一个不想要的值来使用这个变量。

在下面的例子中,在goroutine中使用的index和value的值来自外层作用域。因为goroutine是异步运行的,所以index和value的值可能(而且通常是)与预期的值不同:

mySlice := []string{"A", "B", "C"}
for index, value := range mySlice {
	go func() {
		fmt.Printf("Index: %d\n", index)
		fmt.Printf("Value: %s\n", value)
	}()
}

为了克服这个问题,必须创建一个局部作用域,就像下面的例子一样:

mySlice := []string{"A", "B", "C"}
for index, value := range mySlice {
	index := index
	value := value
	go func() {
		fmt.Printf("Index: %d\n", index)
		fmt.Printf("Value: %s\n", value)
	}()
}

另一种处理方式是将值作为args传递给goroutines:

mySlice := []string{"A", "B", "C"}
for index, value := range mySlice {
	go func(index int, value string) {
		fmt.Printf("Index: %d\n", index)
		fmt.Printf("Value: %s\n", value)
	}(index, value)
}

5.在检查可能出现的错误之前推迟Close

defer 这是Go开发者中的一个常见模式,为实现io.Closer 接口的值,Close() 方法。例如,在打开一个文件时:

f, err := os.Open("/tmp/file.md")
if err != nil {
    return err
}
defer f.Close()

但是这种模式对可写文件是有害的,因为推迟函数调用会忽略其返回值,而Close() 方法可能会返回错误。例如,如果你向文件写了数据,那么在你调用Close 的时候,它可能已经被缓存在内存中而没有被刷到磁盘上。这个错误应该被明确地处理。

虽然你可以完全不使用defer ,但你需要记住在他们的工作完成后关闭该文件。一个更好的方法是defer 一个封装函数,就像下面的例子:

f, err := os.Open("/tmp/file.md")
if err != nil {
	return err
}

defer func() {
	closeErr := f.Close()
	if closeErr != nil {
		if err == nil {
			err = closeErr
		} else {
			log.Println("Error occured while closing the file :", closeErr)
		}
	}
}()
return err

当谈到在团队中工作时,审查其他人的代码变得很重要。DeepSource是一个自动化的代码审查工具,它可以管理端到端的代码扫描过程,只要有新的提交或新的拉动请求被推送,就会自动提出修复请求。

为Go设置DeepSource是非常容易的。只要你设置了它,就会对你的整个代码库进行初步扫描,找到改进的范围,修复它们,并为这些变化打开PR。

go build!