首页 Go channel 的坑
文章
取消

Go channel 的坑

如下表格列出了不同状态下的 channel,做不同操作时的效果,不同效果有标记序号,后文有实例验证。

操作nil channelclosed channelnot nil and not closed channel
阻塞读阻塞(1)buf 中有值,读到值(2);
buf 中无值,读到零值(3)
buf 中有剩余值,读到剩余值(4);buf 中无剩余,阻塞(5)
非阻塞读返回(6)buf 中有值,读到值(7);
buf 中无值,读到零值(8)
buf 中有剩余值,读到剩余值(9);
buf 中无剩余,返回(10)
阻塞写阻塞(11)panic(12)buf 中有空间(13) 或有 goroutine 阻塞读(14),成功写入,返回;
buf无空间且无 goroutine 读(15)或 buf 已满(16),,阻塞
非阻塞写返回(17)panic(18)buf 有空间,写入成功(19);
buf 无空间,写入不成功,返回(20);
closepanic(21)panic(22)成功,返回(23);

阻塞和非阻塞有着不同的效果,我们首先要先能够区分它们的区别,如下。

阻塞和非阻塞的读写

阻塞读

  1. 读取并赋值
    1
    
    val:=<-ch
    
  2. 读取不赋值
    1
    
    <-ch
    
  3. for range 遍历
    1
    2
    3
    
    for val:=<-ch {
     ...
    }
    

非阻塞读

select case 语句:

1
2
3
4
select {
    case val:=<-ch:
        ...
}

阻塞写

1
ch<-val

非阻塞写

1
2
3
4
select{
    case ch<-value:
        ...
}

不同情况下的效果示例

情况(1)

从一个 nil 的 channel 阻塞读:

1
2
3
4
5
6
7
// channel.go

func main() {
	var ch chan int
	val, ok := <-ch
	fmt.Printf("received:%d,ok:%t", val, ok)
}

结果:

1
2
3
4
5
6
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:
main.main()
        channel.go:10 +0x25
exit status 2

由于阻塞导致的死锁。

情况(2)和情况(3)

从一个已关闭的 channel 阻塞读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
	var ch = make(chan int, 5)
	for i := 1; i <= 5; i++ {
		ch <- i
	}
	close(ch)

	for i := 0; i < 5; i++ {
		val := <-ch
		fmt.Printf("read %d \n", val)
	}
	fmt.Println("channel len:", len(ch))
	val, ok := <-ch
	fmt.Printf("read %d,ok:%t \n", val, ok)
}

结果:

1
2
3
4
5
6
7
8
9
// 读取到 buf 中的值
read 1 
read 2 
read 3 
read 4 
read 5 
channel len: 0
// buf 空后,读取到零值
read 0,ok:false 

情况(4)和情况(5)

channel 非 nil,未关闭,阻塞读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
	var ch = make(chan int, 5)
	go func() {
		for i := 1; i <= 4; i++ {
			ch <- i
		}
		fmt.Printf("send 4 data at %s \n", time.Now().Format(time.Stamp))
		time.Sleep(10 * time.Second)
		ch <- 10
		fmt.Printf("send 5th data at %s \n", time.Now().Format(time.Stamp))
	}()

	for i := 0; i < 5; i++ {
		val := <-ch
		fmt.Printf("read %d at %s \n", val, time.Now().Format(time.Stamp))
	}

	time.Sleep(20 * time.Second)
}

结果:

1
2
3
4
5
6
7
8
9
// 直接读取 buf 中的数据
read 1 at Feb 17 15:59:56 
send 4 data at Feb 17 15:59:56 
read 2 at Feb 17 15:59:56 
read 3 at Feb 17 15:59:56 
read 4 at Feb 17 15:59:56 
// 读开始阻塞,直到发送了第5个数据
send 5th data at Feb 17 16:00:06 
read 10 at Feb 17 16:00:06 

情况(6)

从一个 nil 的 channel 非阻塞读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
	var ch chan int

	for {
		select {
		case val, ok := <-ch:
			if ok {
				fmt.Println("select get ", val)
			} else {
				fmt.Println("select get zero value ", val)
			}
			time.Sleep(time.Second)
		default:
			fmt.Println("select nothing")
		}
	}
}

结果:

1
2
3
4
select nothing
select nothing
select nothing
...

情况(7)和情况(8)

从一个已关闭的 channel 非阻塞读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
	var ch = make(chan int, 10)

	go func() {
		for i := 1; i <= 5; i++ {
			ch <- i
			fmt.Println("send ", i)
		}
		close(ch)
		fmt.Println("closed ch,buf length is ", len(ch))
	}()
	for {
		select {
		case val, ok := <-ch:
			if ok {
				fmt.Println("select get ", val)
			} else {
				fmt.Println("select get zero value ", val)
			}
			time.Sleep(time.Second)
		}
	}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
send  1
send  2
send  3
send  4
send  5
closed ch,buf length is  4
select get  1
select get  2
select get  3
select get  4
select get  5
select get zero value  0
select get zero value  0
select get zero value  0
select get zero value  0
...

我们看到在关闭 channel 后,channel 的 buf 中还有待读取的。 接下来,for select 语句先是读取完了写入 channel 的正常值,这符合情况(1); 后面,for select 语句读到的都是 channel的零值(int 的零值是0),这符合情况(2);

情况(9)和情况(10)

channel 非 nil,未关闭,非阻塞读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
	var ch = make(chan int, 10)

	go func() {
		for i := 1; i <= 5; i++ {
			ch <- i
			fmt.Println("send ", i)
		}
	}()
	for {
		select {
		case val, ok := <-ch:
			if ok {
				fmt.Println("select get ", val)
			} else {
				fmt.Println("select get zero value ", val)
			}
			time.Sleep(time.Second)
		default:
			fmt.Println("select nothing")
		}
	}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
send  1
send  2
send  3
send  4
send  5
select get  1
select get  2
select get  3
select get  4
select get  5
select nothing
select nothing
select nothing
...

情况(11)

向一个 nil 的 channel 阻塞写:

1
2
3
4
5
6
7
// channel.go


func main() {
	var ch chan int
	ch <- 10
}

结果:

1
2
3
4
5
6
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send (nil chan)]:
main.main()
        channel.go:5 +0x25
exit status 2

这种情况下是阻塞导致的死锁。

情况(12)

向一个关闭的 channel 阻塞写:

1
2
3
4
5
6
7
8
// channel.go

func main() {
	var ch = make(chan int)
	close(ch)
	ch <- 10
	fmt.Println("send 10")
}

结果:

1
2
3
4
5
6
panic: send on closed channel

goroutine 1 [running]:
main.main()
        channel.go:6 +0x45
exit status 2

情况(13)

channel 非nil,未关闭,buf 有空间,这时阻塞写可以成功:

1
2
3
4
5
func main() {
	var ch = make(chan int, 10)
	ch <- 10
	fmt.Println("send 10")
}

结果:

1
send 10

情况(14)

channel 非nil,未关闭,有 goroutine 阻塞读,这时阻塞写可以成功:

1
2
3
4
5
6
7
8
9
10
func main() {
	var ch = make(chan int)
	go func() {
		time.Sleep(time.Second)
		ch <- 10
		fmt.Println("send 10")
	}()
	val := <-ch
	fmt.Println("read", val)
}

结果:

1
2
send 10
read 10

情况(15)

channel 非nil,且未关闭,channel 无空间,无 goroutine 读,使用阻塞写

1
2
3
4
5
func main() {
	var ch = make(chan int)
	ch <- 10
	fmt.Println("send 10")
}

结果:

1
2
3
4
5
6
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        channel.go:9 +0x31
exit status 2

阻塞导致的死锁。

情况(16)

channel 非nil,且未关闭,channel 空间已满,使用阻塞写

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
package main

import (
	"fmt"
	"time"
)

func main() {
	var ch = make(chan int, 5)

	go func() {
		// 延迟接收
		time.Sleep(5 * time.Second)
		for {
			select {
			case val := <-ch:
				fmt.Printf("receive %d at %s \n", val, time.Now().Format(time.Stamp))
			}
		}
	}()
	for i := 1; i <= 5; i++ {
		ch <- i
	}
	fmt.Println("send 5 data:", time.Now().Format(time.Stamp))

	ch <- 10
	fmt.Println("send 6th data:", time.Now().Format(time.Stamp))

	// 避免主goroutine过快结束
	time.Sleep(20 * time.Second)
}

结果:

1
2
3
4
5
6
7
8
send 5 data: Feb 17 15:12:56
receive 1 at Feb 17 15:13:01 
receive 2 at Feb 17 15:13:01 
receive 3 at Feb 17 15:13:01 
receive 4 at Feb 17 15:13:01 
receive 5 at Feb 17 15:13:01 
receive 10 at Feb 17 15:13:01 
send 6th data: Feb 17 15:13:01

可以看到看到在 channel buf满了的时候,再写入被阻塞了。

情况(17)

channel 为 nil,非阻塞写,写入不成功,直接返回:

1
2
3
4
5
6
7
8
9
func main() {
	var ch chan int
	select {
	case ch <- 10:
		fmt.Println("send 10")
	default:
		fmt.Println("default")
	}
}

结果:

1
default

情况(18)

channel 已关闭,非阻塞写:

1
2
3
4
5
6
7
8
9
10
11
12
// channel.go

func main() {
	var ch = make(chan int)
	close(ch)
	select {
	case ch <- 10:
		fmt.Println("send 10")
	default:
		fmt.Println("default")
	}
}

结果:

1
2
3
4
5
6
panic: send on closed channel

goroutine 1 [running]:
main.main()
        channel.go:9 +0x48
exit status 2

情况(19)

channel buf有空间,非阻塞写,写入成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 func main() {
	var ch = make(chan int, 5)
	for i := 1; i <= 4; i++ {
		ch <- i
	}
	fmt.Printf("channel is not full with %d eles \n", len(ch))

	select {
	case ch <- 10:
		fmt.Println("write 10 to channel")
	default:
		fmt.Println("default")
	}
}

结果:

1
2
channel is not full with 4 eles 
write 10 to channel

情况(20)

channel buf无空间,非阻塞写入不成功,返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
	var ch = make(chan int, 5)
	for i := 1; i <= 5; i++ {
		ch <- i
	}
	fmt.Printf("channel is full with %d eles \n", len(ch))

	select {
	case ch <- 10:
		fmt.Println("write 1 to channel")
	default:
		fmt.Println("default")
	}
}

结果:

1
2
channel is full with 5 eles 
default

channel buf 满了后,10 没有写进去,而是执行了 default。

情况(21)

关闭一个 nil 的 channel:

1
2
3
4
5
6
7
// channel.go

func main() {
	var ch chan int
	close(ch)
	fmt.Println("closed channel")
}

结果:

1
2
3
4
5
6
panic: close of nil channel

goroutine 1 [running]:
main.main()
        channel.go:7 +0x1b
exit status 2

情况(22)

关闭一个已经关闭的 channel:

1
2
3
4
5
6
7
func main() {
	var ch = make(chan int)
	close(ch)
	fmt.Println("closed channel")
	close(ch)
	fmt.Println("closed channel again")
}

结果:

1
2
3
4
5
6
7
closed channel
panic: close of closed channel

goroutine 1 [running]:
main.main()
        channel.go:9 +0x78
exit status 2

情况(23)

channel 非nil,未关闭,关闭 channel,正常。

1
2
3
4
5
func main() {
	var ch = make(chan int)
	close(ch)
	fmt.Println("closed channel")
}

结果:

1
closed channel

避免对 nil channel 操作

对 nil channel 操作没有意义。阻塞读写会导致阻塞,甚至死锁;非阻塞读写直接返回,达不到目的。close 更会直接 panic。

避免对 closed channel 写操作和重复 close

这些情况都会造成 panic。

那么如何知道 channel 是否 closed 呢?没有很好的方法。

对于 sender 和 receiver 在数量上通常有 4 种关系:

  • 1:1
  • 1:n
  • m:1
  • m:n

对于前两种,可以由 sender 在发送完数据后进行 close,receiver 读取数据时使用 val,ok:=<-ch 判断下 ok 是否为 false,false 时即表示数据读取完成。

对于第三种,可以使用一个额外的 channel,由 receiver 写入数据,通知给 sender 不要再发数据了:

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
func main() {
	var ch = make(chan int)
	var stopCh = make(chan struct{})

	// 10 个 sender
	for i := 0; i < 10; i++ {
		go func() {
			for {
				select {
				case <-stopCh:
					return
				case ch <- rand.Intn(100):
				}
			}
		}()
	}

	// 1 个 receiver
	go func() {
		i := 0
		for val := range ch {
			if i >= 10 {
                // 通知该结束了
				close(stopCh)
				break
			}
			fmt.Println("received:", val)
			i++
		}
	}()

	// 避免主 goroutine 过快结束
	time.Sleep(20 * time.Second)
}

结果:

received: 81
received: 87
received: 47
received: 59
received: 81
received: 18
received: 25
received: 40
received: 56
received: 0
sender send data
sender send data
sender send data
sender get stop signal
sender get stop signal
sender send data
sender send data
sender send data
sender send data
sender get stop signal
sender get stop signal
sender get stop signal
sender get stop signal
sender send data
sender get stop signal
sender send data
sender get stop signal
sender send data
sender get stop signal
sender send data
sender get stop signal

这里sender 接受到 stopCh 后没有 close ch,因为 channel 重复 close 的话就会 panic,我们直接退出 goroutine 即可,不管 channel 有没有 close,都会被 GC 回收。简而言之,不管 channel,退出 goroutine,让 GC 处理。

对于第四种,TODO

读取 channel 判断 OK

读取 channel 时,可能读到的是关闭后的零值,需判断 ok 是否为 true,为 true 即为写入的正常数据,为 false 为零值,零值可能对业务来说没有意义。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
	var ch = make(chan int, 5)

	go func() {
		for i := 0; i < 12; i++ {
			money := rand.Intn(1000) + 5000
			ch <- money
		}
		close(ch)
	}()

	for {
		select {
		case money, ok := <-ch:
			if ok {
				fmt.Prinf("收到工资:%d,嘿嘿 \n", money)
			} else {
				fmt.Println("公司不发工资了,不干了!")
				return
			}
		}
	}
}
本文由作者按照 CC BY 4.0 进行授权