Golang|Select 误用带来的错觉

2021年9月5日 0 条评论 1.49k 次阅读 1 人点赞

select 一般都是搭配 channel 使用的。这本身我相信作为Go开发的同学都清楚。

有一道面试题:

如果以下 c1 c2 这两个管道同时有数据写入,那么会优先写入哪个值呢?

package main

import (
	"fmt"
	"github.com/xhyonline/xutil/helper"
)

func main() {

	ch := make(chan int, 2)

	select {
	case ch <- 1:
		fmt.Println("第一个 case")
	case ch <- 2:
		fmt.Println("第二个 case")
	}

}

我相信这道基础题,大部分开发同学不用想都知道。(PS:当管道同时有数据写入时 Go 会根据自己的特点随机选择 c1c2 中的任意一个管道写入数据。)

因此上面的代码既有可能输出 第一个 case 也有可能输出 第二个 case

因此这可能就会给我们带来一种错觉。认为 select 同时只会执行一个 case

这本质上没错,但是错误的使用可能就会造成内存泄露事故。

你再耐心看看下面的代码:

package main

import (
	"fmt"
	"github.com/xhyonline/xutil/helper"
)

func main() {

	ch := make(chan int, 2)

	select {
	case ch <- GetRandom():
		fmt.Println("第一个 case")
	case ch <- GetRandom():
		fmt.Println("第二个 case")
	}

}

func GetRandom() int {
	fmt.Println("调用了我")
	return helper.GetRandom(10)
}

你觉得上面这段代码会输出什么?

结果如下:

调用了我
调用了我
第一个 case

为什么会被 GetRandom() 这个方法会被调用了两次?

来翻阅一下 Select 的说明吧

For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the “select” statement. The result is a set of channels to receive from or send to, and the corresponding values to send. Any side effects in that evaluation will occur irrespective of which (if any) communication operation is selected to proceed. Expressions on the left-hand side of a RecvStmt with a short variable declaration or assignment are not yet evaluated.

简单的来说,这是因为 chan 被发送的表达式都会被求值,因此右手边表达式GetRandom 会被先执行,如果拿到结果后再选择 case 进行执行。

但是这就完了吗?细细品味一下,这种误操作很有可能造成内存泄露。

下面的代码会造成内存泄露,结合上面的解释,你能找到原因吗?

package main

import (
	"fmt"
	"time"
)

func main() {
	// 关闭信号
	var sigClose = make(chan struct{}, 1)

	go func() {
		// 持续写入 buffer 如果遇到关闭信号就退出
		buffer := make([]byte, 0)
	FOR:
		for {
			select {
			case <-sigClose:
				fmt.Println("接受到关闭信号")
				break FOR
			default:
				buffer = append(buffer, 1)
			}
		}
	}()
	
FOR:
	for {
		select {
		// 3 秒后执行关闭操作
		case <-time.After(3 * time.Second):
			close(sigClose)
			break FOR
		default:
			fmt.Println("default")
		}
		time.Sleep(time.Second)
	}

	// 避免程序退出
	var wg=sync.WaitGroup{}
	wg.Add(1)
	wg.Wait()
}

你会发现上面的代码陷入了死循环,不停的在打印 default 并且程序的内存是不停上升的。

原因是表达式 time.After() 会得到一个<-chan Time ,结合上文, 因此你的每一次循环会优先执行 time.After() 而得到一个新的 chan ,所以每一次时间都被重置了。

因此正确的做法是将结果直接付给 select 去做逻辑判断,而不是在 select 语句中书写表达式。

正确的代码如下:

	after := time.After(3 * time.Second)
FOR:
	for {
		select {
		// 3 秒后执行
		case <-after:
			close(sigClose)
			break FOR
		default:
			fmt.Println("default")
		}
		time.Sleep(time.Second)
	}

兰陵美酒郁金香

大道至简 Simplicity is the ultimate form of sophistication.

文章评论(0)

你必须 登录 才能发表评论