遇到 go 中 for range 的一个坑

349 阅读3分钟

corn 调度

本来想开发一个任务调度的服务,目标能够定时去运行一些任务。于是愉快的使用了 corn 包,并根据官网提供的 Demo 编写了自己的调度任务。

func main(){
   c := cron.New(cron.WithSeconds())
   c.AddFunc("*/5 * * * * *", func() {
       fmt.Printf("我是任务1, time =%d\n",time.Now().Unix())
   })
   c.AddFunc("*/1 * * * * *", func() {
        fmt.Printf("我是任务2)
   })
   c.Start()
   select{}
}

调用 corn 对象的 AddFunc 方法就可以添加定时任务,分别设置了任务1每5秒执行一次,任务2每1秒执行一次。就这样很正常执行出结果。

动态加入调度

当我有很多个定时任务时,总不可能是有一个写一个 AddFunc 方法。所以可以使用反射方法,反射得到某个结构体的的方法,然后调用该方法。至于每个定时任务何时触发,可以使用 map 做一个定时任务名和定时时间的做 key-value 对应。

package task
//实现一个含有很多定时任务的结构体
type Task struct {}
func (Task) SyncTask1() {
	log.Printf("我是任务1 time = %d\n", time.Now().Unix())
}

func (Task) SyncTask2() {
	log.Printf("我是任务2")
}

package main
func main(){
var syncList = map[string]string{
		"SyncTask1": "*/5 * * * * *",
		"SyncTask2": "*/1 * * * * *",
	}
	funcs := reflect.ValueOf(&Task.Task{})
	c := cron.New(cron.WithSeconds())
	for key, val := range syncList {
		c.AddFunc(val, func() {
			f := funcs.MethodByName(key)
			f.Call(nil)
		})
		c.Start()
		time.Sleep(time.Second * 3)
	}
	select {}
}

本开开心心的执行以上代码,但是结果没有得到像图1上面的结果而是出现只调度执行任务2。

为什么只有任务2 执行

一开始我以为因为 for 循环这样动态加入定时任务 AddFunc 的原因导致其只执行一次,于是找了官网查看该方法说明,没有说需要什么参数去控制调度,只要调用 AddFunc 方法就会向管理器中加入定时任务,而后所有的定时任务就会依次执行。

于是将问题转移到 for range 循环上,在循环中打印出结果,打印时每次都能打印出值,没什么问题,但是如果打印 value 的内存地址时会发现 2次循环的内存地址都是一样。我增加内容,无论多少个值,value 的内存地址都是一样。

	var arr = []string{"hi", "name", "asas", "sasa", "ffd"}
	for key, value := range arr {
		fmt.Println(key, value)
		fmt.Println(&key, &value)
	}

执行结果说明了,在 for 循环中其创建的变量是共享同一块内存地址。所以每次 key 、value 的内存地址都是一样的。如果 for 循环内的业务逻辑是同步的,就不会有什么影响,如果是异步的就会有影响。cron 每次创建一个定时任务都会创建一个新的 goroutine 来执行。这样就导致定时任务触发时访问到 value 都是 for 循环最后一次的值。

解决方案

for 循环中创建变量共享内存,那只需要每次循环时都创建一个新的变量,就可以很好的解决该问题。

for key, value := range arr {
	tempKey, tempValue := key, value
    ...
}

感觉很多面试公司在笔试中很喜欢出这种问题的面试题,如果没有了解到,很可能就会导致错误。

本文正在参加「金石计划 . 瓜分6万现金大奖」