chan 的使用

chan 的使用

chan 即 channel,不过它更像 java 的 queue,先进先出,这里,简单介绍下 chan 的相关使用。

chan 与缓冲

所谓缓冲,其实就是 chan 的容量,如果容量是 1,那么写入 1 个数据后,chan 就满了,如果这个数据没被读取走,再次写入时会阻塞;如果没设置容量或容量设置为 0,除非有 goroutines 在读取数据,否则写入时直接阻塞。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func TestWithoutBuffer(t *testing.T) {
	myChan := make(chan int)
	//	myChan := make(chan int, 0)

	go func() {
		for {
			i := <-myChan
			t.Logf("read from chan %d", i)
		}
	}()

	for i := 0; i < 10; i++ {
		myChan <- i
	}
}

这里 myChan := make(chan int) 等价于 myChan := make(chan int, 0) chan 即使无缓冲(size 为 0)也可以使用,但必须要有协程在读取,否则直接死锁,如果有缓冲,即使暂无协程读取,也可以写入,比如 size 设置为 10,然后写入 10 个数字,写入完成后再读取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func TestWithBuffer(t *testing.T) {
	myChan := make(chan int, 10)

	for i := 0; i < 10; i++ {
		myChan <- i
	}

	for i := 0; i < 10; i++ {
		t.Logf("read from chan %d", <-myChan)
	}
}

这里,如果 size 设置小于 10,则死锁,缓冲的作用名如其人,主要是为了避免写入到 chan 时阻塞;

chan 的读取与写入

chan 的读取很简单,比如 i := <- myChan 但这可能读取到错误的值,考虑如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func TestWithClose(t *testing.T) {
	myChan := make(chan int)

	go func() {
		for i := 0; i < 20; i++ {
			tmp := <-myChan
			t.Logf("read from chan %d", tmp)
		}
	}()

	for i := 0; i < 9; i++ {
		myChan <- i
	}
	close(myChan)

	time.Sleep(5 * time.Second)
}

写入 0-8 后,关闭 chan,但读取 20 个,会发现前 9 个是 0-8,后面 11 个全是 0;一般的,读取 chan 建议用
i, ok := <- myChan 这里,如果 ok 为 false 说明 chan 关闭;

使用 for-range 读取 chan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func TestWithClose(t *testing.T) {
	myChan := make(chan int)

	go func() {
		for i := range myChan {
			t.Logf("read from chan %d", i)
		}
		t.Log("end for-range")
	}()

	for i := 0; i < 9; i++ {
		myChan <- i
	}
	close(myChan)

	time.Sleep(5 * time.Second)
}

使用 for-range 读取 chan 时,可以不用管 chan 是否已关闭,如果关闭 for 循环会结束,如果没关闭,但无数据,则会阻塞。

使用 select 来读 chan

select 类似 linux io 读写的 select ,即:多路复用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func TestSelectRead(t *testing.T) {
	myChan := make(chan int, 10)
	ticker := time.NewTicker(500 * time.Millisecond)

	go func() {
		for {
			select {
			case i := <-myChan:
				t.Logf("read from chan %d", i)
			case i := <-ticker.C:
				t.Logf("read from ticker %d", i.UnixMilli())
			}
		}
	}()

	for i := 0; i < 10; i++ {
		myChan <- i
		time.Sleep(time.Second)
	}
}

这里,select 会监听多个事件,任何一个发生都会触发,一般用于超时,比如读取 5 秒,没数据则返回,或写入但 5 秒内没写入成功,则告警;使用 select 读取时,要警惕 chan close,如果 close ,然后读取,会快速返回而不是阻塞,一般的,并不建议直接 close chan,应该用 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
func TestSelectRead(t *testing.T) {
	myChan := make(chan int, 10)
	ticker := time.NewTicker(500 * time.Millisecond)

	ctx, cancel := context.WithCancel(context.Background())

	go func() {
	loop:
		for {
			select {
			case i := <-myChan:
				t.Logf("read from chan %d", i)
			case i := <-ticker.C:
				t.Logf("read from ticker %d", i.UnixMilli())
			case <-ctx.Done():
				t.Logf("read from context done")
				break loop
			}
		}
		t.Log("exit for-range")
	}()

	for i := 0; i < 10; i++ {
		myChan <- i
		time.Sleep(time.Second)
	}
	cancel()
	time.Sleep(5 * time.Second)
}

使用 select 来写入 chan

select 写入 chan 时,如果 chan 满了,则不会执行,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func TestSelectWrite(t *testing.T) {
	myChan := make(chan int, 2)

	selectWrite := func(value int) bool {
		select {
		case myChan <- value:
			return true
		default:
			return false
		}
	}

	for i := 0; i < 10; i++ {
		t.Logf("send to chan result %t", selectWrite(i))
	}
}

前 2 个输出为 true,后面均为 false,不会阻塞,不分场景,比如一些无关紧要的数据,如果可以丢失,可以考虑用 select 来写入。

chan 总结

  1. chan 可以指定缓冲区大小,关闭后读取不会 panic,但关闭后写入会 panic;
  2. 可以使用 for-range 来读取 chan,不用去关心 chan 是否关闭的问题;
  3. 如果要非阻塞的写入,考虑用 select;
  4. 如果 chan 没有 goroutines 持有,则 go 垃圾回收会回收内存,不建议显示 close,建议用 context;
  5. chan 先进先出,但如果有多个 goroutines 读取时,仍需要考虑并发问题;

默认触发的 ticker

考虑如下代码:

1
2
3
4
5
6
7
func TestTicker(t *testing.T) {
	ticker := time.NewTicker(5 * time.Second)
	t.Logf("%s begin the ticker", time.Now().Format(time.DateTime))
	for ; ; <-ticker.C {
		t.Logf("%s on ticker", time.Now().Format(time.DateTime))
	}
}

由于 for 循环第一个是初始化,第二个是条件,第 3 个是 for 循环执行完成后执行,以上代码可实现默认触发;

最后更新于