Go 语言中一些常见的错误

记录一些 Go 语言中常见的错误。

无限递归调用、 没有初始化 map, slice, channel 就进行赋值、 给结构体添加方法的时候没有使用指针、 goroutine 里直接使用循环中的变量、 忽略关闭文件时可能出现的错误...

# 1. 无限递归调用

出现无限递归调用的情况一种是忘记退出,另一种是刻意为之,都会导致系统内存耗尽。

函数调用时会在内存形成调用记录(call frame),保存调用位置和内部变量等信息。 函数内调用其他函数会在原来的调用记录上方创建新的调用记录,被调用函数退出时调用记录才会被删除。 所有的调用记录形成调用栈(call stack),无限递归调用会让调用栈一直增长。

把调用函数放到最后一步执行称为尾调用。有些语言会提供尾调用优化 (opens new window),只保留内层函数的调用记录,避免调用栈持续增长。但 Go 没有,所以无限递归调用会导致 系统内存 耗尽,因为 Go 程序并不会限制它的内存 (opens new window)。而且似乎目前也 没法在程序内设置内存限制 (opens new window)

# 2. 没有初始化 map, slice, channel 就进行赋值

引用类型的零值是 nil,默认都没有分配内存,需要先使用 make 分配内存才可以读写。
读写 nil slice 会导致 index out of range 错误。
写入 nil map 会导致 assignment to entry in nil map 错误。
接收或写入 nil chan 会导致 deadlock,关闭会导致 panic

所以一般不要使用 var m map[string]string 这种声明变量,而应该直接使用 m := make(map[string]string) 直接创建。

# 3. 给结构体添加方法的时候没有使用指针

当方法修改了接收器内部的值的时候,使用非指针的话是不会改变原本的对象的。这是个潜在的风险。所以应该使用指针。

举个 🌰:

type User struct {
	Balance int
}

// 错误示范
func (u User) PaySalary1() {
	u.Balance += 1_000_000
}

// 正确示范
func (u *User) PaySalary2() {
	u.Balance += 1_000_000
}

func main() {
	u1 := User{}
	u1.PaySalary1()

	u2 := User{}
	u2.PaySalary2()

	fmt.Println(u1.Balance) // 0
	fmt.Println(u2.Balance) // 1000000
}

# 4. goroutine 里直接使用循环中的变量

循环体中的变量每次迭代都会重新赋值,所以直接在 goroutine 里使用,可能会出现预期之外的结果。
编辑器也会有 loop variable i captured by func literal 的警告。

// 错误示范
// 可能会输出多个 9
for i := 0; i < 9; i++ {
  go func() {
    fmt.Print(i)
  }()
}

// 正确示范
// 1~8都会输出一次
for i := 0; i < 9; i++ {
  go func(i int) {
    fmt.Print(i)
  }(i)
}
for i := 0; i < 9; i++ {
  i := i
  go func() {
    fmt.Print(i)
  }()
}

# 5. 忽略关闭文件时可能出现的错误

打开文件,然后 defer f.Close() 几乎成了范式,但当有一种情况是写入文件时没有报错,关闭文件的时候报错了,这时候这个错误被忽略就会导致数据丢失。

因为当你调用 f.Write(xxx) 的时候,是先写入缓存中(这时就返回成功了),系统会自己规划时机将缓存写入磁盘。这是个异步操作,关闭文件是系统告诉你错误的最后时机了。

// 错误示范
func saveLog0() error {
  f, err := os.Open("log.txt")
  if err != nil {
    return err
  }
  defer f.Close()

  _, err = f.WriteString("peace")
  return err
}

// 正确示范1
// 调用多次 f.Close 不会有任何问题
func saveLog1() error {
	f, err := os.Open("log.txt")
	if err != nil {
		return err
	}
	defer f.Close()

	if _, err := f.WriteString("peace"); err != nil {
		return err
	}

	if err := f.Close(); err != nil {
		return err
	}

	return err
}

// 正确示范2
// 强制让系统写入磁盘可能有性能问题,这时候忽略 f.Close 的错误没问题
func saveLog2() error {
	f, err := os.Open("log.txt")
	if err != nil {
		return err
	}
	defer f.Close()

	if _, err := f.WriteString("peace"); err != nil {
		return err
	}

	return f.Sync()
}

# 参考

5 Common mistakes in Go (opens new window)

Don't defer Close() on writable files (opens new window)