The Go language’s powerful goroutine feature makes developers love it, and how do multiple goroutines communicate? It is through Channel to do so. This article teaches you two ways to read data from Channel and the timing of using it, and uses a case to quickly understand what problems will be encountered in Channel practice. The following two examples will help you understand how to read data from Channel.
There are two ways to read a channel
The first one is the for range
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
}
|
The second is via the v, ok := <-ch
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
for {
v, ok := <-ch
if !ok {
return
}
fmt.Println(v)
}
}
|
After reading the above two examples, developers know clearly these two reading methods, but they will encounter when to start using the first one and when to use the second one? Let’s take a look at a simple example.
Two goroutines read each other’s characters
Let’s take a look at the topic, there is a string foobar, split the character into a channel, and use two goroutines to read the character interchangeably.
1
2
3
4
5
6
|
goroutine01: f
goroutine02: o
goroutine01: o
goroutine02: b
goroutine01: a
goroutine02: r
|
First copy the above topic to main.go
, you can look at the example below to see how to write two goroutine, you can first in online practice to see, do not look down to answer.
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 (
"sync"
)
func main() {
str := []byte("foobar")
ch := make(chan byte, len(str))
wg := &sync.WaitGroup{}
wg.Add(2)
for i := 0; i < len(str); i++ {
ch <- str[i]
}
go func() {
}()
go func() {
}()
wg.Wait()
}
|
After reading this question, you should know that you can’t use method 1 to read channel data, because for range
will continue to read data until the channel is closed, so there is no guarantee that another gorountine can correctly read the next character.
How to do it
From the above example, we can see that the code written inside the two goroutines should be the same, so we need a channel to notify the next goroutine to read.
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
package main
import (
"fmt"
"sync"
)
func main() {
str := []byte("foobar")
ch := make(chan byte, len(str))
next := make(chan struct{})
wg := &sync.WaitGroup{}
wg.Add(2)
for i := 0; i < len(str); i++ {
ch <- str[i]
}
close(ch)
go func() {
defer wg.Done()
for {
<-next
v, ok := <-ch
if ok {
fmt.Println("goroutine01:", string(v))
} else {
close(next)
return
}
next <- struct{}{}
}
}()
go func() {
defer wg.Done()
for {
<-next
v, ok := <-ch
if ok {
fmt.Println("goroutine02:", string(v))
} else {
close(next)
return
}
next <- struct{}{}
}
}()
next <- struct{}{}
wg.Wait()
}
|
- First of all, after all data is written to the channel, the channel needs to be closed.
- Next Channel is added to notify the next goroutine to read the data.
- main main function should drop data to next channel first.
- Close the next channel when the ch channel is finished reading data.
After executing the above steps, you will get the following result.
1
2
3
4
5
6
7
|
goroutine02: f
goroutine01: o
goroutine02: o
goroutine01: b
goroutine02: a
goroutine01: r
panic: close of closed channel
|
Here you can see that <-next will always have data after the channel is closed, so we need to use another way to determine if the channel is closed, so we will change it as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if ok {
fmt.Println("goroutine01:", string(v))
} else {
close(next)
return
}
next <- stop
}
}()
|
The program runs correctly, but we see the if else
code, so we can refactor it again.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if !ok {
close(next)
return
}
fmt.Println("goroutine01:", string(v))
next <- stop
}
}()
|
The final full code is shown below. You can run it online (https://go.dev/play/p/G6nkoIIHAky).
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
package main
import (
"fmt"
"sync"
)
func main() {
str := []byte("foobar")
ch := make(chan byte, len(str))
next := make(chan struct{})
wg := &sync.WaitGroup{}
wg.Add(2)
for i := 0; i < len(str); i++ {
ch <- str[i]
}
close(ch)
go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if !ok {
close(next)
return
}
fmt.Println("goroutine01:", string(v))
next <- stop
}
}()
go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if !ok {
close(next)
return
}
fmt.Println("goroutine02:", string(v))
next <- stop
}
}()
next <- struct{}{}
wg.Wait()
}
|
Summary
Through the above example, we hope that you can understand the characteristics of Channel for those who are new to it. In addition to this example, you can think about how to implement worker pool pattern, and I will introduce this part to you when I have a chance.