Golang | 常见错误案例

Original Text

其他错误

WaitGroup is reused before previous Wait has returned.

Go 文件中定义了 waitGroup 的全局变量,且在并发环境下被调用,导致 waitGroup 没有重置就直接被重用了。单元测试时写上对原方法的并发测试,可以很轻易复现。

  1. sync.WaitGroup 本身支持重用,但是未结束就被重用则会 Panic,提示 panic: sync: WaitGroup is reused before previous Wait has returned。需要注意并发场景下 WaitGroup 的使用,最好是重新定义 WaitGroup 比较好。
  2. 尽量避免将 sync.WaitGroup 定义为全局变量。

参考资料:

常见错误

初级篇

  1. 左大括号 { 不能单独放一行。编译器会在除了 { 符号结尾的每行代码结尾加上 ; 符号,标识一行的结束。
  2. 函数体中的变量必须使用,全局变量可以不使用。变量声明之后仅有赋值操作,不代表使用。
  3. import 一个包之后,必须使用到其中的变量、函数、接口或者结构体;如果都没有,仅需要其中的 init 函数,只需要使用 _ 下划线符号作为别名来忽略导入的包,就可以避免编译错误。
  4. 简短声明 := 的变量仅能在函数内部使用,不能定义在函数外部。
  5. 不能用简短声明方式来单独为一个变量重复声明,:= 左侧至少有一个新变量,才允许多变量的重复声明。
  6. 不能使用简短声明 := 来赋值一个结构体字段的值。
  7. 简短声明 := 一个同名的已有的变量,仅会在新的作用域下生效,即简短声明并不等于赋值。常用的 IDE 通常也会给出 Declaration of 'x' shadows declaration at xxx.go 的提示。

  1. 显式类型的变量无法使用 nil 来初始化。nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。但声明时不指定类型,编译器也无法推断出变量的具体类型。即 var x = nil 是不允许的,会在编译期提示 use of untyped nil in variable declaration 错误。
  2. 使用 Flag 命令行参数之后,需要记得调用 flag.Parse() 方法,否则命令行输入的值将不会传递到 flag 变量中。参数不仅可以通过命令行来输入,还可以通过读取配置文件来输入。参考内部框架的实现,使用 Viper 读取工程默认的配置项文件和参数,并且通过 Unmarshal 来反序列化配置项并加载到内存中。如果是自定义读取过程并写入 init 函数中,只需要在项目的 main 函数中导入 init 函数所在的项目路径即可。
  3. 允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素则会造成运行时 panic,提示 assignment to entry in nil map。
  4. 在创建 map 类型的变量时可以指定容量,但不能像 slice 一样使用 cap() 来检测分配空间的大小,会在编译期提示错误 error: invalid argument m1 (type map[string]int) for cap
  5. string 类型的变量值不能为 nil,字符串类型的零值或默认值是空串 ""
  6. 使用 array 类型的值作为函数参数,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的。如果需要改变数组的值,可以传递数组的指针,即类似于 *[<len>]int 的类型。另外,如果想要修改数组的值,可以使用不带长度的 slice,即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据。因为 slice 副本中的 array 指针与原 slice 指向同一个地址,所以当修改副本 slice 的元素时,原 slice 的元素值也会被修改。可以理解为 slice 本身就是一个指向 array 的指针。
  7. 使用 Go 语言的 range 遍历 slice 和 array 时,会返回 2 个值,第一个是元素索引,第二个是元素的值。如果仅使用一个变量接收,那么将只有元素索引。
  8. slice 和 array 其实是一维数据。如果要创建多维数组,就性能和复杂度而言,用 Go 实现的效果并不理想。创建多维数组,可以创建外部的 slice,再向内部的 slice 添加互相独立的切片。也可以先创建一个大的一维数组,再根据二维数组的行数使用 slice[x:y]的方式得到新的子切片。
  9. 访问 map 中不存在的 key,会返回元素对应数据类型的零值。如果需要检查 key 是否存在,可以用 map 直接访问,检查返回的第二个参数是否为 true 即可。
  10. string 类型的值是常量,不可更改。如果要更改 string,可以将 string 转换为 rune 数组,修改指定下标值之后再转换为 string 即可。
func main() {
	x := "text"
	xRunes := []rune(x)
	xRunes[0] = 'T'  // 这里也可以使用中文等
	x = string(xRunes)
	fmt.Println(x) // Text
}
  1. string 与 byte slice 之间的转换,参与转换的是原始数据拷贝出来的副本。这一点和新旧 slice 共享底层数组不同。Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:① 在 map[string] 中查找 key 时,使用了对应的 []byte,避免做 m[string(key)] 的内存分配;② 使用 for range 迭代 string 转换为 []byte的结果变量:for i,v := range []byte(str) {...},这里的 []byte 变量直接使用原始的 string 的数据。
  2. 对字符串用索引访问返回的不是字符,而是一个 byte 值,这一点和 php 一致。如果想要访问的结果是某个字符,可以直接使用 string 进行强制转换。
func main() {
	x := "ascii"
	fmt.Println(x[0])         // 97
	fmt.Println(string(x[0])) // a
	fmt.Printf("%T\n", x[0])  // uint8
}
  1. 字符串并不都是 UTF8 文本。string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。判断字符串是否是 UTF8 文本,可使用 unicode/utf8 包中的 ValidString() 函数。
func main() {
	str1 := "ABC"
	fmt.Println(utf8.ValidString(str1)) // true

	str2 := "A\xfeC"
	fmt.Println(utf8.ValidString(str2)) // false

	str3 := "A\\xfeC"
	fmt.Println(utf8.ValidString(str3)) // true	// 把转义字符转义成字面值
}
  1. Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。如果要得到字符串的字符数,可使用 unicode/utf8 包中的 RuneCountInString(str string) (n int) 函数,或者转换为 []rune() 后再统计 rune 数组的长度,两者效果是一致的。
  2. 在多行 array、slice、map 语句中要注意添加 , 号,声明语句中 } 折叠到单行后,尾部的 , 不是必需的。
  3. log.Fatallog.Panic 不只是 log。log 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal*()Panic*() 时能做更多日志外的事,如中断程序的执行。
  4. 对内建数据结构的操作并不是同步的。尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,开发者需自己保证变量等数据以原子操作更新。要保证并发安全,可以使用 Goroutine 和 channel,同时 sync 包中也提供了锁。
  5. range 迭代 string 时得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都会直接使用 0XFFFD rune(�) Unicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。
func main() {
	data := "A\xfe\x02\xff\x04"
	for _, v := range data {
		fmt.Printf("%#x ", v) // 0x41 0xfffd 0x2 0xfffd 0x4	// 错误
	}
	
	for _, v := range []byte(data) {
		fmt.Printf("%#x ", v) // 0x41 0xfe 0x2 0xff 0x4	// 正确
	}
}
  1. range 迭代 map 时可能每次迭代得到的顺序都不一样。直接迭代 map,无法保证每次得到的顺序一致,Go 的运行时是有意打乱迭代顺序的,但并不是每次都会打乱。
  2. switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 的代码块。同时 case 也可以是多条件判断。
func main() {
	isSpace := func(char byte) bool {
		switch char {
		case '\t':
			return true
		case ' ': // 空格符会直接 break,返回 false // 和其他语言不一样
			//fallthrough // 返回 true
		case 'r':
			return true
		}
		return false
	}

	fmt.Println(isSpace('\t')) // true
	fmt.Println(isSpace(' '))  // false
}
  1. Go 的自增和自减操作只有后置,没有前置的 ++-- 运算,同时 ++-- 只作为运算符而非表达式。
// 错误示例
func main() {
	data := []int{1, 2, 3}
	i := 0
	++i // syntax error: unexpected ++, expecting }
	fmt.Println(data[i++]) // syntax error: unexpected ++, expecting :
}

// 正确示例
func main() {
	data := []int{1, 2, 3}
	i := 0
	i++
	fmt.Println(data[i]) // 2
}
  1. Go 的按位取反和其他编程语言不一样。很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 用 ^ XOR 操作符来按位取反。同时 ^ 也是按位异或(XOR)操作符,多次连续的 ^ 运算也是允许的,^^ 运算就会恢复原状。Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。
// 错误的取反操作
func main() {
	fmt.Println(~2) // bitwise complement operator is ^
}

// 正确示例
func main() {
	var d uint8 = 2
	fmt.Printf("%08b\n", d)  // 00000010
	fmt.Printf("%08b\n", ^d) // 11111101
}
  1. 除了按位清除运算符 bit clear(&^),Go 也有很多和其他语言一样的位操作符,但优先级和其他语言有差异。
func main() {
	fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n", 0x2&0x2+0x4) // & 优先 +
	//prints: 0x2 & 0x2 + 0x4 -> 0x6
	//Go:    (0x2 & 0x2) + 0x4
	//C++:    0x2 & (0x2 + 0x4) -> 0x2

	fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n", 0x2+0x2<<0x1) // << 优先 +
	//prints: 0x2 + 0x2 << 0x1 -> 0x6
	//Go:     0x2 + (0x2 << 0x1)
	//C++:   (0x2 + 0x2) << 0x1 -> 0x8

	fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n", 0xf|0x2^0x2) // | 优先 ^
	//prints: 0xf | 0x2 ^ 0x2 -> 0xd
	//Go:    (0xf | 0x2) ^ 0x2
	//C++:    0xf | (0x2 ^ 0x2) -> 0xf
}
  1. 不导出的 struct 字段无法被 encode。以小写字母开头的 struct 成员字段,是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时只能得到零值。
  2. 程序退出时还有 Goroutine 在执行,这在 Go 语言中是正常的。Go 程序默认不等所有 Goroutine 都执行完才退出,这点需要特别注意。如果要解决这个问题,可以引入 sync.WaitGroup 来同步各个协程。
// 主程序会直接退出
func main() {
	workerCount := 2
	for i := 0; i < workerCount; i++ {
		go doIt(i)
	}
	time.Sleep(1 * time.Second)
	fmt.Println("all done!")

	//[1] is running
	//[0] is running
	//all done!
}

func doIt(workerID int) {
	fmt.Printf("[%v] is running\n", workerID)
	time.Sleep(3 * time.Second) // 模拟 goroutine 正在执行
	fmt.Printf("[%v] is done\n", workerID)
}
  1. 向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回。只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。
func main() {
	ch := make(chan string)

	go func() {
		for m := range ch {
			fmt.Println("Processed:", m)
			time.Sleep(1 * time.Second) // 模拟需要长时间运行的操作
		}
	}()

	ch <- "cmd.1"
	ch <- "cmd.2" // 不会被接收处理
}
  1. 向已关闭的 channel 发送数据会造成 panic,但是从已关闭的 channel 中接收数据是安全的。
func main() {
	ch := make(chan int)
	for i := 0; i < 3; i++ {
		go func(idx int) {
			ch <- idx
		}(i)
	}

	fmt.Println(<-ch)           // 输出第一个发送的值
	close(ch)                   // 还有其他的 sender 时不能关闭, panic: send on closed channel
	time.Sleep(2 * time.Second) // 模拟做其他的操作
}
  1. 使用了一个值为 nil 的 channel 会导致死锁。在一个值为 nil 的 channel 上发送和接收数据将永久阻塞,运行时会提示 fatal error: all goroutines are asleep - deadlock!
  2. 若函数 receiver 传参是传值方式,则无法修改参数的原有值。方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。如果是指针类型的值,那么在使用时还是有可能发生改变的。
type data struct {
	num   int
	key   *string
	items map[string]bool
}

func (this *data) pointerFunc() {
	this.num = 7
	*this.key = "pointerFunc.key"
	this.items["pointerFunc"] = true
}

func (this data) valueFunc() {
	this.num = 8
	*this.key = "valueFunc.key"
	this.items["valueFunc"] = true
}

func main() {
	key := "key1"

	d := data{1, &key, make(map[string]bool)}
	fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

	d.pointerFunc() // 修改 num 的值为 7
	// num=7  key=key1  items=map[]
	fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

	d.valueFunc() // 修改 key 和 items 的值, 但是 num 的值不会改变
	// num=7  key=valueFunc.key  items=map[valueFunc:true]
	fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
}

中级篇

  1. 关闭 HTTP 的响应体。使用 HTTP 标准库发起请求、获取响应时,即使不从响应中读取任何数据或响应为空,都需要手动关闭响应体。在关闭时,需要注意判断 response.Body 是否为空,否则请求失败时 response.Body 为空,直接关闭会造成 panic。绝大多数请求失败的情况下,resp 的值为 nil 且 err 为 non-nil。但如果得到的是重定向错误,那它俩的值都是 non-nil,最后依旧可能发生内存泄露。有 2 个解决办法:① 可以直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体;② 手动调用 defer 来关闭响应体。

resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。

// 正确示例
func main() {
	resp, err := http.Get("https://www.baidu.com")

	// 关闭 resp.Body 的正确姿势
	if resp != nil {
		defer resp.Body.Close()
	}

	checkError(err)
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	checkError(err)

	fmt.Println(string(body))
}
  1. 关闭 HTTP 连接。一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接,但标准库 net/http 的连接默认只在服务器主动要求关闭时才断开,所以程序可能会将 socket 描述符消耗完。解决办法有两个:① 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接 request.Close = true。② 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接 request.Header.Add("Connection", "close")

这里第一个方法和第二个方法是等价的,只需要使用一个即可。这里最好根据使用场景决定。如果需要向同一服务器发送大量请求,使用默认的保持长连接即可;如果需要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接,或者调整 Linux 系统的大文件打开数 fs.file-max 的值。

  1. 将 JSON 中的数字 encode/decode 为 interface{} 类型时,Go 默认会将数值当做 float64 处理。如果尝试解码的 JSON 字段是整型,有这些方式:① 将 int 统一转换为 float64 使用;② 将 decode 后的 float64 当做 int 来使用;③ 使用 struct 类型将需要的数据映射为数值;④ 使用 struct 将数值类型映射为 json.RawMessage 原生数据类型。
  2. 要注意 struct、array、slice 和 map 的值比较。可以使用相等运算符 == 来比较结构体变量,前提是两个结构体的成员都是可比较的类型。如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。

Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 reflect.DeepEqual()方法,这种方法使用到了反射,所以可能会比较慢。可比较的类型:布尔、整型、浮点数、复数、字符串、指针值、通道值(如果是由同一个 make 创建的,或者两者都为 nil,则相等)、接口值、接口与非接口值。不可比较的类型:map、slice、func、struct,只能判断是否为 nil,彼此之间不能比较。参考:https://blog.csdn.net/qmhball/article/details/113771087

  1. 从 panic 中恢复。在一个 defer 延迟执行的函数中调用 recover(),它便能捕捉 / 中断 panic。需要注意的是,recover() 仅在 defer 执行的函数中调用才会生效。
  2. 在 range 迭代 slice、array、map 时通过更新引用来更新元素。在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址。如果要修改原有元素的值,应该使用元素直接访问。如果集合保存的就是指针值,那么可以直接通过 range 循环得到元素值,来直接更新原值。
  3. 新旧 slice 的互相引用。从 slice 中重新部分切片得到新 slice 时,新 slice 会引用原 slice 的底层数组。如果跳了这个坑,程序可能会分配大量的临时 slice 来指向原底层数组的部分数据,将导致难以预料的内存使用。可以通过拷贝临时 slice 的数据,而不是重新切片来解决。拷贝只需要使用 copy 函数即可。
  4. slice 中数据的误用。使用重新切片得到的新 slice,如果再去修改原有的 slice,会导致新得到的切片值也会改变。因为两者使用的是同一个底层数组,所以修改原底层数组,也会导致新得到的切片改变。
// 错误使用 slice 的拼接示例
func main() {
	path := []byte("AAAA/BBBBBBBBB")
	sepIndex := bytes.IndexByte(path, '/') // 4
	println(sepIndex)

	dir1 := path[:sepIndex]
	dir2 := path[sepIndex+1:]
	println("dir1: ", string(dir1)) // AAAA
	println("dir2: ", string(dir2)) // BBBBBBBBB

	dir1 = append(dir1, "suffix"...)
	println("current path: ", string(path)) // AAAAsuffixBBBB

	path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
	println("dir1: ", string(dir1)) // AAAAsuffix
	println("dir2: ", string(dir2)) // uffixBBBB,此时 dir2 取值 [4+1:],所以 path 为 AAAAsuffix/uffixBBBB

	// 可以得到 dir2 只是保存了原有切片的切割 index 位置
	println("new path: ", string(path)) // AAAAsuffix/uffixBBBB	// 错误结果
}

要避免这种情况发生,有两种常用的方法:① 重新分配新的 slice 并拷贝需要的数据;② 使用完整的 slice 表达式input[low:high:max],容量便跟随调整为 max - low,这个时候得到的 slice 已经在固定范围确认了,不会因为添加而发生改变。

// 使用 full slice expression
func main() {
	path := []byte("AAAA/BBBBBBBBB")
	sepIndex := bytes.IndexByte(path, '/') // 4
	dir1 := path[:sepIndex:sepIndex]       // 此时 cap(dir1) 指定为4, 而不是先前的 16
	dir2 := path[sepIndex+1:]
	dir1 = append(dir1, "suffix"...)

	path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
	println("dir1: ", string(dir1))     // AAAAsuffix
	println("dir2: ", string(dir2))     // BBBBBBBBB
	println("new path: ", string(path)) // AAAAsuffix/BBBBBBBBB
}
  1. 新旧 slice 问题。当从一个已存在的旧 slice 中创建新 slice 时,二者的数据指向相同的底层数组。如果程序依赖这个特性,需要注意 stale slice 问题。当向其中一个 slice 追加元素而其指向的底层数组容量不足时,会重新分配一个数组来存储数据,而其他 slice 还指向原来的旧底层数组。
  2. 类型声明与方法。从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法。如果需要使用原类型的方法,可以将原类型以匿名字段的形式嵌入到定义的新 struct 中,这样 interface 类型声明也保留它的方法集。
// 定义 Mutex 的自定义类型
type myMutex sync.Mutex

func main() {
	var mtx myMutex
	mtx.Lock()   // 不会继承 Mutex 的 Lock 和 Unlock 方法
	mtx.UnLock() // mtx.UnLock() undefined (type myMutex has no field or method Lock)
}

定义原类型的匿名字段,可以继承原类型的方法。

// 正确做法:类型以字段形式直接嵌入
type myLocker struct {
	sync.Mutex
}

func main() {
	var locker myLocker
	locker.Lock()
	locker.Unlock()
}
  1. 使用 break 跳出 for-switch 和 for-select 代码块,如果没有指定标签的 break,只会跳出内层循环。使用 goto 最好跳转到循环体之后的地方。
// break 配合 label 跳出指定代码块
func main() {
	for {
		switch {
		case true:
			fmt.Println("breaking out...")
			break // 死循环,一直打印 breaking out...
            // 解决:switch 块后再加一个 break 或者 break label
		}
	}

    // 不会执行到
	fmt.Println("out...")
}
  1. for 语句中的迭代变量与闭包函数。for 语句中的迭代变量在每次迭代中都会重用,即 for 中创建的闭包函数接收到的参数始终是同一个变量,在 Goroutine 开始执行时都会得到同一个迭代值。常用的解决方法有两种:① 无需修改 Goroutine 函数,在 for 内部使用局部变量保存迭代值,再传参;② 直接将当前的迭代值以参数形式传递给匿名函数。还有一种特殊情况,当迭代的元素值就是指针时,可以直接进行使用。
// 正确示例
func main() {
	data := []*field{{"one"}, {"two"}, {"three"}}
	for _, v := range data { // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同
		go print(*v)
	}
	time.Sleep(2 * time.Second)
	// 输出 one two three
}

注意:Go 语言这一与其他语言不同的特性,在 Go 1.22 版本中已经被修改。Go 1.22 将经典三段式 for 循环语句以及 for range 语句中的用短声明形式定义的循环变量,从整个循环定义和共享一个变为每轮迭代重新创建一个,这样每轮循环使用的变量地址都不是同一个了。参考:https://zhuanlan.zhihu.com/p/682791946

Go 1.22 的这一改动,会影响 for 循环的效率。但是可以通过提前定义迭代变量,使用临时变量接收 for 循环迭代变量的方式,来实现和修改前相近的效率,也能实现每轮循环使用一个单独变量的效果。

  1. defer 函数的参数值:对 defer 延迟执行的函数,它的参数会在声明时就计算出具体值,而不是在执行时才求值。
  2. defer 函数执行时机:对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。如果在一个长时间执行的函数中,内部 for 循环中使用 defer 来清理每次迭代产生的资源调用,就是不正确的。可以通过将 defer 延迟执行的逻辑写入匿名函数中,来解决这个问题。
  3. 断言失败情况:在类型断言语句中,断言失败则会返回目标类型的零值,断言变量与原来变量混用可能出现异常情况。也就是说,在断言的返回值中,尽量不要使用和原值一样的变量名,避免断言失败造成原值的覆盖。
// 错误示例
func main() {
	var data interface{} = "great"

	data, ok := data.(int)
	// data 混用
	if ok {
		fmt.Println("[is an int], data: ", data)
	} else {
		fmt.Println("[not an int], data: ", data) // [isn't a int], data:  0
	}

	fmt.Println(data) // 输出 0,此时原值丢失
}
  1. 阻塞的 Goroutine 与资源泄露。现在有一个从完整数据集中,获取第一条数据的函数。
func First(query string, replicas []Search) Result {
	c := make(chan Result)
	replicaSearch := func(i int) { c <- replicas[i](query) }
	for i := range replicas {
		go replicaSearch(i)
	}
	return <-c
}

在搜索重复时,每次都会起一个 Goroutine 去处理,每个 Goroutine 都会将搜索结果发到结果 channel 中,channel 中收到的第一条数据会直接返回。返回完第一条数据后,其他 Goroutine 的搜索结果得不到处理,协程会被阻塞,导致资源泄露。

在 First 函数中,结果 channel 是无缓冲的,这意味着只有第一个 Goroutine 能返回,由于没有 receiver,其他的 Goroutine 会在发送上一直阻塞。大量调用之后,就可能造成资源泄露。为了避免资源泄露,应该确保所有的 Goroutine 都能正常退出。这里有 3 种解决方法。

① 使用带缓冲的 channel,并确保所有的 Goroutine 返回结构都能接收。

func First(query string, replicas ...Search) Result {
	c := make(chan Result, len(replicas))
	searchReplica := func(i int) { c <- replicas[i](query) }
	for i := range replicas {
		go searchReplica(i)
	}
	return <-c
}

② 使用 select 语句,配合能保存一个缓冲值 channel default 语句。default 的缓冲 channel 保证了即使结果 channel 收不到数据,也不会阻塞 Goroutine。

func First(query string, replicas ...Search) Result {
	c := make(chan Result, 1)
	searchReplica := func(i int) {
		select {
		case c <- replicas[i](query):
		default:
		}
	}
	for i := range replicas {
		go searchReplica(i)
	}
	return <-c
}

③ 使用特殊的废弃(cancellation) channel 来中断剩余 Goroutine 的执行。

func First(query string, replicas ...Search) Result {
	c := make(chan Result)
	done := make(chan struct{})
	defer close(done)
	searchReplica := func(i int) {
		select {
		case c <- replicas[i](query):
		case <-done:
		}
	}
	for i := range replicas {
		go searchReplica(i)
	}

	return <-c
}

高级篇

  1. 使用指针作为方法的 receiver。只要值是可寻址的,就可以在值上直接调用指针方法。即是对一个方法,它的 receiver 是指针就足矣。但不是所有值都是可寻址的,比如 map 类型的元素、通过 interface 引用的变量。
type data struct {
	name string
}

type printer interface {
	print()
}

func (p *data) print() {
	fmt.Println("name: ", p.name)
}

func main() {
	d1 := data{"one"}
	d1.print() // d1 变量可寻址,可直接调用指针 receiver 的方法

	var in printer = data{"two"}
	in.print() // 类型不匹配

	m := map[string]data{
		"x": data{"three"},
	}
	m["x"].print() // m["x"] 是不可寻址的	// 变动频繁
}
  1. 更新 map 字段的值。如果 map 一个字段的值是 struct 类型,则无法直接更新该 struct 的单个字段,只能更新整个 struct。因为 map 中的元素是不可寻址的,需要区分开的是,slice 中的元素是可寻址的。如果需要更新 map 中 struct 元素的值,有两个方法:① 使用局部变量,更新局部变量的字段值之后再写回;② 使用指向 struct 元素的指针,当做 map 的值。
  2. nil interface 和 nil interface 值。虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil。如果 interface 变量的值是跟随其他变量变化的,那么在与 nil 比较时需要小心。也就是说,如果函数值返回为 interface{} 类型时,需要返回空值时,直接返回 nil,而不是返回 var result *struct{} = nil 这样的变量值,因为携带了 struct 类型,所以返回时,得到的结果不为 nil。
  3. 与 C++ 不同,在 Go 中即使使用 new()make() 来创建变量,变量为内存分配位置依旧归 Go 编译器管。Go 编译器会根据变量的大小及其 escape analysis 逃逸分析的结果来决定变量的存储位置,故能准确返回本地变量的地址,这在 C/C++ 中是不行的。在 go build 或 go run 时,加入 -m 参数,能准确分析程序的变量分配位置。
  4. GOMAXPROCS、Concurrency(并发)and Parallelism(并行)。Go 1.4 及以下版本,程序只会使用 1 个执行上下文 / OS 线程,即任何时间都最多只有 1 个 goroutine 在执行。Go 1.5 版本将可执行上下文的数量设置为 runtime.NumCPU() 返回的逻辑 CPU 核心数,这个数与系统实际总的 CPU 逻辑核心数是否一致,取决于你的 CPU 分配给程序的核心数,可以使用 GOMAXPROCS 环境变量或者动态的使用 runtime.GOMAXPROCS() 来调整。需要注意的是,GOMAXPROCS 的值是可以超过 CPU 的实际数量的,在 1.5 中最大为 256。
  5. 读写操作的重新排序。Go 可能会重排一些操作的执行顺序,可以保证在一个 Goroutine 中操作是顺序执行的,但不保证多 Goroutine 的执行顺序。如果需要保证多 Goroutine 的代码执行顺序,可以使用 channel 或 sync 包中的锁机制等。这一点和 Java 是类似的,Java 也仅保证代码的单线程执行结果,即便单线程执行的代码段是重排序的,但不会影响执行的结果。Go 仅保证单个 Goroutine 的顺序执行。
  6. 优先调度问题。程序如果出现一个 Goroutine 在运行时阻止了其他 Goroutine 的运行,比如程序中有一个不让调度器运行的 for 循环,这个时候如果不触发调度器执行,那么将会出现问题。调度器会在 GC、Go 声明、阻塞 channel、阻塞系统调用和锁操作后再执行,也会在非内联函数调用时执行。
func main() {
	done := false

	go func() {
		done = true
	}()

	for !done {
	}

	println("done !")  // 直接打印结束
}

可以添加 -m 参数来分析 for 代码块中调用的内联函数。也可以直接使用 runtime 包中的 Gosched() 来手动启动调度器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值