自己总结的一些 Go 协程的小技巧
以下的代码,除特殊说明外,均由我编写,如有错误,还请大佬多多指正
控制协程 我在学习 Go 时,在别人的代码里时常看到类似make(chan struct{})
的语句,后来明白这样的值可以用来控制协程,下面的例子使用通道来控制协程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package mainimport ( "fmt" "strconv" "sync" "time" ) var ( stop chan struct {} wait sync.WaitGroup run chan string ) func goRun () { for { select { case in := <-run: fmt.Println(in) case <-stop: fmt.Println("I am stop" ) wait.Done() return } } } func main () { stop = make (chan struct {}) run = make (chan string ) wait.Add(1 ) go goRun() go func () { for i := 1 ; i <= 10 ; i++ { run <- "I am running x" + strconv.Itoa(i) time.Sleep(time.Second) } close (stop) }() wait.Wait() }
其中struct{}
是空结构体,占用空间为零,close(stop)
时 select 的case <-stop:
不再阻塞,返回struct{}
的零值(struct 零值就是本身),协程返回。如果是stop <- struct{}{}
协程同样会收到消息进而退出,另外也可以使用 Context:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package mainimport ( "context" "fmt" "strconv" "time" ) var ( run chan string ) func goRun (ctx context.Context) { for { select { case in := <-run: fmt.Println(in) case <-ctx.Done(): fmt.Println("I am stop" ) return } } } func main () { run = make (chan string ) ctx, cancel := context.WithCancel(context.Background()) go goRun(ctx) go func () { for i := 1 ; i <= 10 ; i++ { run <- "I am running x" + strconv.Itoa(i) time.Sleep(time.Second) } cancel() }() time.Sleep(time.Second * 12 ) }
使用 Context 可以方便一些,Context 还有其它用法,这里就不展开了
定时任务 前段时间有个需求,需要每天定时执行重置任务(运行某个函数),然后发现了一种方法可以满足要求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func reset () { fmt.Println("do something..." ) } func AutoReset (hour int ) { if hour < 0 || hour > 23 { return } fmt.Printf("每天在 %d 时重置\n" , hour) for { timeLocation, _ := time.LoadLocation("Asia/Shanghai" ) next := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), hour, 0 , 0 , 0 , timeLocation) subTime := next.Sub(time.Now()) if subTime <= 0 { subTime += 24 * 60 * 60 * 1000000000 } t := time.NewTimer(subTime) <-t.C reset() } }
使用方法(每天在 19 时重置):
上面的代码虽然只可以精确到小时执行,但只要稍加改造,就可以控制到其它的时间单位
range 陷阱 举一个例子,下面的例子想通过 chan 接受十个数据,然后每个数据启动一个 goroutine 来打印它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func Server (queue chan int ) { for req := range queue { go func () { fmt.Println(req) }() } } func main () { q := make (chan int ) go Server(q) for i := 1 ; i <= 10 ; i++ { q <- i } time.Sleep(time.Second * 20 ) }
预期的结果应该是包含1-10
的打印结果,但是实际发现打印的值里有很多重复的,也就是说有不同的 goroutine 访问了同一个数据,而如果我们添加这样一行代码,变成这样,发现问题就解决了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func Server (queue chan int ) { for req := range queue { go func () { fmt.Println(req) }() } } func main () { q := make (chan int ) go Server(q) for i := 1 ; i <= 10 ; i++ { q <- i time.Sleep(time.Second) } time.Sleep(time.Second * 20 ) }
如果注释掉time.Sleep(time.Second)
,就有可能出现多个 goroutine 访问同一个 req,原因在于(我理解的) :由于发送给 queue 数据的速率很快,导致 req 的值更新时不同的 goroutine 还在访问这唯一的 req,在更深的层次上,我们应该由地址考虑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func Server (queue chan int ) { for req := range queue { fmt.Println("Out: " , &req, " " , req) go func () { fmt.Println("In: " , &req, " " , req) }() } } func main () { q := make (chan int ) go Server(q) for i := 1 ; i <= 10 ; i++ { q <- i time.Sleep(time.Second * 2 ) } time.Sleep(time.Second * 20 ) }
可以发现所有的地址是相同的,即都是这唯一的 req,正确的修复方法有些两种(我更喜欢第二种):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func Server (queue chan int ) { for req := range queue { req := req fmt.Println("Out: " , &req, " " , req) go func () { fmt.Println("In: " , &req, " " , req) }() } } func Server (queue chan int ) { for req := range queue { fmt.Println("Out: " , &req, " " , req) go func (req int ) { fmt.Println("In: " , &req, " " , req) }(req) } }
可以看到无论是哪种方法,传入 goroutine 的参数的地址是不一样的,即参数不是同一个参数,而在错误的示范里,这些参数都相同,导致不同 goroutine 访问的是同一个变化的参数