最近搞出来一个挺弱智的问题,记录一下,以示警戒。代码主要逻辑如下,使用 select和time.After()实现超时控制,doSomething()一定会返回结果。

 sign := make(chan struct{})
 go func() {
        doSomething()
        sign <- struct{}
        close(sign)
 }()
 select {
 case <-time.After(timeout):
        fmt.Println("timeout")
        return
 case <-sign:
        fmt.Println("succ")
        return
 }

实际执行过程中,会发现goroutine数量在不断增长,很明显这段代码发生了协程泄漏。 这段代码新启goroutine的地方只有一处,就是这个go func,goroutine数量不断增长说明goroutine一直没有退出。 继续看,为什么协程没有退出呢,代码里唯一会产生阻塞的地方就是这个sign <- struct{}通过channel通知函数执行完了,正常无超时的情况下,select里能正常消费这个channel,协程能正常退出,但是一旦发生超时,select直接退出,sign channel就没有消费者了,由于是无缓冲channel,写入方就会被阻塞导致协程无法退出,造成协程泄漏。及其低级的一个错误,属于是对go基础掌握不牢了。 正确的实现方式,创建一个有缓冲channel就可以了

 sign := make(chan struct{}, 1)
 go func() {
        doSomething()
        sign <- struct{}{}
        close(sign)
 }()
 select {
 case <-time.After(timeout):
        fmt.Println("timeout")
        return
 case <-sign:
        fmt.Println("succ")
        return
 }

另外,也可以使用context实现超时控制

sign := make(chan struct{}, 1)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
 go func() {
        doSomething()
        sign <- struct{}{}
        close(sign)
 }()
 select {
 case <-ctx.Done():
        fmt.Println("timeout")
        return
 case <-sign:
        fmt.Println("done")
        return
 }

需要明确的一点是,以上的超时控制并不会影响到已经启动的goroutine的执行,即便超时启动的goroutine还是会继续执行直到退出,这里的超时只是你可以不用阻塞在这苦苦等待已启动的这个goroutine执行完,你可以继续执行其他逻辑。如果超时后我想立即退出goroutine该怎么办呢,这里是做不到立即退出的,不过可以把ctx传到goroutine里,每执行一步就判断下ctx是否已经超时了,如果超时就退出,一定程度控制goroutine的执行。

    sign := make(chan struct{}, 1)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	go func(ctx context.Context) {
		defer close(sign)
		doSomething()
		if ctx.Err() != nil {
			fmt.Println("goroutine exit")
			return
		}
		fmt.Println("other")
		doSomeOtherThing()
		sign <- struct{}{}
	}(ctx)
	select {
	case <-ctx.Done():
		fmt.Println("timeout")
	case <-sign:
		fmt.Println("done")
	}