golang 天生支持高并发场景,其核心在于 go routine, go channel, WaitGroup, Mutex 的配合使用。本人前三个月转行 golang,在 go routine, go channel, WaitGroup 使用中遇到许多坑。为此我总结了一些常见的 go 并发编程的错误模板。
1. 忘记向 go routine 传参
本来打算开10个 go routine, 分别打印0-9
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(2 * time.Second)
}
点击运行后,发现10次输出并不是0到9各输出一次,而是某个数字被输出了很多次, 最小的输出并不是0 ,最大的输出甚至是 10。其原因在于部分 go routine 开启后正准备去获取 i, 而此时 i 已经 经过了 好几轮 for 循环了。
解决方法: 向 go routine 内部传参, 每开 一个 go routine,就往里面传参。
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
time.Sleep(2 * time.Second)
}
2. 不带缓冲的 go channel 读写时机不一致 导致死锁
package main
import "fmt"
func main() {
c := make(chan string)
c <- "hello,world"
s := <-c
fmt.Println(s)
}
点击运行, 直接报错死锁。其原因在于,对于不带缓冲的 go channel 必需读操作 先于写操作,这样做的目的是防止 go channel 里面的东西 没有人去读。
解决方法1:新开一个 go routine 提前准备好读取 go channel
package main
import "fmt"
func main() {
c := make(chan string)
go func(c chan string) {
s := <-c
fmt.Println(s)
}(c)
c <- "hello,world"
}
解决方法2: 使用带缓冲的 go channel
package main
import "fmt"
func main() {
c := make(chan string, 1)
c <- "hello,world"
s := <-c
fmt.Println(s)
}
3. 忘记使用 WaiGroup 三件套: Add, Done, Wait 方法,导致主线程提前结束。
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
}
解决方法: 使用WaiGroup 三件套。
WaitGroup是用来等待协程完成的 一个类。其常用的方法有三个,add, done, wait。
WaitGroup.add(1) 表示告诉 主线程 等待一个 gorouinte 完成才能结束, 要不然主线程早早结束了,而go toutine 里面的任务还没有完成。比如在代码的 for 循环里面,每开一个 go routine 之前,都要调用 WaitGroup.add(1)方法, 表示告诉主线程先别着急结束,我这里又有一个新任务,你先等着。
WaitGroup.Done() 表示告诉主线程, 我这里的go routine执行结束了,你不用等我了。你可以将 WaitGroup.Done() 看作是 WaitGroup.add(-1) 而 defer WaitGroup.Done() 表示在 这个 go routine 结束之前一定要告诉外面这个 go routine 结束了。defer 表示最后一步执行。
WaitGroup.Wait() 表示主线程在这里等待所有 go routine 完成任务,后面的代码先不执行。
package main
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
wg.Wait()
}
4. channel 忘记关闭, 没有 go routine往里写, 但是有 go routine 一直在读, 导致死锁。
package main
import (
"fmt"
"sync"
)
func main() {
c := make(chan int, 10)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
c <- i
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for x := range c {
fmt.Println(x)
}
}()
wg.Wait()
}
第一个 go routine 往channel里面写入10个 int, 第二个channel一直从 channel 读,当channel为空时, 这个 go routine 还一直在等待(因为 wg.Wait)
解决方法: 第一个 go routine 执行完以后, 主动关闭 channel, 这样第二个 go routine 在读的时候如果发现 channel 已经被关闭了 就会结束。
package main
import (
"fmt"
"sync"
)
func main() {
c := make(chan int, 10)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer close(c)
defer wg.Done()
for i := 0; i < 10; i++ {
c <- i
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for x := range c {
fmt.Println(x)
}
}()
wg.Wait()
}
5. 两个 go channel 你读我,我读你,相互等待死锁。
package main
import "sync"
func main() {
ch1 := make(chan int,0)
ch2:= make(chan int,0)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
select {
case <- ch1:
ch2<- 100
}
}()
wg.Add(1)
go func() {
defer wg.Done()
select {
case <- ch2:
ch1 <- 100
}
}()
wg.Wait()
}
解决方法:给ch1 或者 ch2 任意一个channel 初始化写入一个数
package main
import (
"fmt"
"sync"
)
func main() {
ch1 := make(chan int,1)
ch2:= make(chan int,1)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
temp:=<- ch1
ch2 <- temp
fmt.Println("Read from ch1 and then send to ch2" )
}()
wg.Add(1)
go func() {
defer wg.Done()
temp:=<- ch2
ch1 <- temp
fmt.Println("Read from ch2 and then send to ch1" )
}()
// 下面两行代码注释掉一行
//ch1<-1
ch2<-2
wg.Wait()
}
6. channel 没有被写入 , select 语句中不写 defafult 造成死锁
package main
import (
"fmt"
)
func main() {
c1 := make(chan int, 1)
c2 := make(chan int, 1)
select {
case int1 := <-c1:
fmt.Println("c1 received: ", int1)
case int2 := <-c2:
fmt.Println("c2 received: ", int2)
}
}
解决方法, 写入 channel, 或者增加default 语句
package main
import (
"fmt"
)
func main() {
c1 := make(chan int, 1)
c2 := make(chan int, 1)
c2 <- 1 // 这条语句和 default 语句注释一条即可
select {
case int1 := <-c1:
fmt.Println("c1 received: ", int1)
case int2 := <-c2:
fmt.Println("c2 received: ", int2)
default: // default 语句 和 c2<-1 注释一条即可
fmt.Println("No channel received.")
}
}
7. 多个 go routine 对同一个 数据进行修改不上锁,造成写冲突
package main
import (
"fmt"
"time"
)
func main() {
var count int
for i := 0; i < 1000; i++ {
go func() {
count = count + 1
time.Sleep(10)
}()
}
time.Sleep(time.Second)
fmt.Println(count)
}
预计输出 1000,但每次运行结果不一样,原因是 修改 count变量的 go routine 还没来得及操作就被 另一个 go routine 抢先操作。
解决方法,对 count 变量进行上锁。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var count int
var mu sync.Mutex
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
count = count + 1
mu.Unlock()
time.Sleep(10)
}()
}
time.Sleep(time.Second)
fmt.Println(count)
}