Channel


==「不要通过共享内存来通信,而应该通过通信来共享内存」==

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的
  2. 发送操作和接收操作中,对元素值的处理是不可分割的
  3. 发送操作在完全完成之前会被阻塞,接受操作也是这样
  4. 元素值从外界进入通道时会被赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src) // 延迟关闭 channel
for i := 0; i < 10; i++ {
src <- i // 把元素放入 src 通道
}
}()
go func() {
defer close(dest) // 延迟关闭 channel
for i := range src { // 从 src 通道取出元素
dest <- i * i // 把平分数放入 dest 通道
}
}()
for i := range dest { // 取出平方数并打印
//复杂操作
println(i)
}
}
1
2
print:
0 1 4 9 16 25 36 49 ..

缓存

  • 缓存通道

    1
    make(chan int , 10)
  • 非缓存通道

    1
    make(chan int)

    非缓存通道中只能接收一个值,这个值如果没有输出,那么直接会把整个通道堵塞,而缓存通道则是会在通道缓存满了再通道堵塞

    堵塞的这个性质非常重要,可以实现一些意想不到的事情

    为什么 ==done==一个非缓存通道 可以发送一个中断信号

    因为done通道实际上是被用作一个信号量,而不是用来存储数据。在这种情况下,<- done表达式用于从通道中接收消息,但由于通道是空的,所以实际上这里接收的是一个特殊的值,表示通道被关闭或者发送方已经完成了发送操作

    可以用于 超时操作

    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
    35
    36
    37
    38
    39
    40
    41
    var w sync.WaitGroup
    用 channel 模拟击球游戏

    func init() {
    rand.Seed(time.Now().UnixNano())
    }

    func main() {
    court := make(chan int)

    w.Wait()
    go play("Nadal", court)
    go play("Djokovic", court)

    court <- 1

    w.Wait()
    }

    func play(name string, court chan int) {
    defer w.Done()
    for {
    ball, ok := <-court
    if !ok {
    println("Player", name, "won")
    return
    }

    n := rand.Intn(100)
    if n%13 == 0 {
    println("Player", name, "missed")
    close(court)
    return
    }

    println("Player", name, "hit", ball)
    ball++

    court <- ball
    }
    }

单向通道

类型的声明包含接收符 ==<-chan==,就是单向通道

1
2
3
4
// 这里的ch的参数就是个 只接收参数的单向通道
func SendInt (ch chan<-int){
ch <- rand.Intn(1000)
}
1
2
3
4
// 这里的ch参数就是个 只输出参数的单向通道
func PutInt(ch <-chan int){
fmt.Println(<-ch)
}

什么时候会panic

  • 对关闭的通道发送数据、

  • 对关闭的通道再次关闭、

关闭后的通道的性质

关闭通道是指关闭通道的入口,通道关闭后仍可以从中取出数据

如果接收两个值,第二个值是 bool ,表示还能不能取出元素

如果值为 false ,表示通道已经关闭并且没有元素了,此时第一个元素会是零值

如果值为 true ,表示成功地取出了元素,注意你此时无法判断通道是否关闭

因此使用这个 bool 值判断通道是否关闭是有延迟的

永远在发送方关闭通道,不能在接收方关闭通道

可以从forrange中获取通道中的值

1
2
3
for elem := range Chan {
fmt.Println(elem)
}

Select 和 Chan 连用

1
2
3
4
5
6
7
8
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
fmt.Println("没有收到任何消息")
}

select 语句只能与通道联用,是一种多路通信选择的控制结构,允许一个 goroutine 等待多个通信操作

它由若干个分支组成。每次执行这种语句的时候,只有一个分支中的接收/发送代码会被运行

  • 如果所有分支都阻塞,则会运行 defult 分支

    (也就是说含有默认分支的 select 永远不会阻塞

  • 如果所有分支都阻塞,又没有 defult 分支,则会一直阻塞,直到有分支可以执行

  • 如果同时有多个分支可以执行,则会随机选择一个

select 用于多个channel监听并收发消息,当任何一个case满足条件则会执行,若没有可执行的case,就会执行default,如果没有default,程序就会阻塞。

注意事项

  • 如果通道关闭了,接收并不会阻塞,而同时获得零值与 false

    所以如果发现通道关闭了,应当及时屏蔽对应的分支或者采取其他措施

    (将通道赋值为 nil 可以屏蔽改对应的分支了,因为 nil 的通道是一直阻塞的)

  • select 语句只会将某分支的通道操作运行一次,所以如果你想连续操作的话,可以在 for 中使用 select

    但是如果你在 select 中使用 break 的话,只会跳出当前 select,而 for 并不会跳出

    (如果想将 for 也跳出,可以在 for 前面放一个 label,再将 labelbreak 一起使用)

  • 尽管 select 本身是并发安全的,但是不代表你的 case 表达式和分支中的代码也是并发安全的

应用


定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"
import "time"

/*
type Ticker struct {
C <-chan Time
r runtimeTimer
}
*/
// Ticker.C是一个单向通道噢,所以可以使用range
func main() {
t := time.NewTicker(time.Second)
for v := range t.C {
fmt.Println("hello, ", v)
}
}
1
2
3
4
5
6
7
8
# output :
C:\Users\xxx\AppData\Local\JetBrains\GoLand2023.2\tmp\GoLand\___6go_build_today.exe
hello, 2024-04-08 14:00:25.2584533 +0800 CST m=+1.014317801
hello, 2024-04-08 14:00:26.2587477 +0800 CST m=+2.014612201
hello, 2024-04-08 14:00:27.2658846 +0800 CST m=+3.021749101
hello, 2024-04-08 14:00:28.267351 +0800 CST m=+4.023215501
hello, 2024-04-08 14:00:29.2610047 +0800 CST m=+5.016869201
....

一次定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"
import "time"

/*
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
*/
func main() {
select {
case <-time.After(time.Second):
fmt.Println("after")
}
}
1
2
3
4
# output : 
C:\Users\xxx\AppData\Local\JetBrains\GoLand2023.2\tmp\GoLand\___6go_build_today.exe
after

超时控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"time"
)

func queryDB(ch chan int) {
//time.Sleep(1 * time.Second)
ch <- 100
}

func main() {
ch := make(chan int)
go queryDB(ch)
t := time.NewTicker(1 * time.Second)

select {
case _ = <-t.C:
fmt.Println("timeout")
case v := <-ch:
fmt.Println("ch:", v)
}
}
1
2
# output : 
ch : 100

如果想要实现超时,则把那个注释去掉

1
2
# output :
timeout

样例

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

import (
"fmt"
"time"
)

func main() {
messages := make(chan int, 10)
done := make(chan bool)

defer close(messages)

go func() {
ticker := time.NewTicker(time.Second)

for _ = range ticker.C {
select {
case <-done:
fmt.Println("done")
return
default:
fmt.Println("send message", <-messages)
}
}
}()

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

time.Sleep(5 * time.Second)
close(done)
time.Sleep(1 * time.Second)
fmt.Println("exit")
}
1
2
3
4
5
6
7
8
9
# output : 
C:\Users\xxx\AppData\Local\JetBrains\GoLand2023.2\tmp\GoLand\___6go_build_today.exe
send message 0
send message 1
send message 2
send message 3
done
exit
# close(done),非缓存通道会输出一个中断信号