golang channel 用法总结
goroutine
使用golang的channel之前,我们需要先了解go的goroutine。
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,相比线程开销更小,完全由 Go 语言负责调度,是 Go 支持并发的核心。
如下所示,在go中我们可以很方便的开启并发执行。
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("goroutine message")
fmt.Println("main function message")
time.Sleep(time.Second) //休眠1s
}
channel
通道(channel)则是用来传递数据的一个数据结构。 大部分时候 channel 都是和 goroutine 一起配合使用。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
chan T // 可以接收和发送类型为 T 的数据, 定义时使用
chan<- float64 // 只可以用来发送 float64 类型的数据, 在函数参数中使用, 这样可以限定chan使用
<-chan int // 只可以用来接收 int 类型的数据, 在函数参数中使用, 这样可以限定chan使用
无缓冲channel
- 我们可以使用如下的方式声明一个无缓冲区的channel。其中int代表这个通道传递的是int类型。除了int、string、float等基本类型外,channel传递的类型还可以是自定义的结构体或别名等。
c := make(chan int) //声明一个int类型的无缓冲通道
type NewType uint8
c1 := make(chan NewType) //声明自定义类型的无缓冲通道
- 通道最基本的用法就是在多个协程之间传递消息。channel是线程安全的,即在使用过程中,有多个协程同时向一个channel发送数据,或读取数据是完全可行的,不需要额外的操作。
- 无缓冲的通道只有当发送方和接收方都准备好时才会传送数据,否则准备好的一方将会被阻塞。
- 我们来看如下这个例子:我们声明了一个无缓冲的channel,然后开启一个协程向这个channel发送数据,另外主线程则从这个channel中读取数据。我们观察程序的输出可以发现,在主线程休眠期间,协程是阻塞在发送向通道发送数据的地方,只有当主线程休眠结束开始从channel中读取数据时,协程才开始向下运行。同样的,当协程发送完第一个数据休眠时,主线程读取了第一个数据,准备从channel中读取第二个数据时会被阻塞,知道协程休眠结束向通道发送数据后才会继续运行。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) //声明一个int类型的无缓冲通道
go func() {
fmt.Println("ready to send in g1")
c <- 1
fmt.Println("send 1 to chan")
fmt.Println("goroutine start sleep 1 second")
time.Sleep(time.Second)
fmt.Println("goroutine end sleep")
c <- 2
fmt.Println("send 2 to chan")
}()
fmt.Println("main thread start sleep 1 second")
time.Sleep(time.Second)
fmt.Println("main thread end sleep")
i := <- c
fmt.Printf("receive %d\n", i)
i = <- c
fmt.Printf("receive %d\n", i)
time.Sleep(time.Second)
}
输出:
- 由于channel这种阻塞发送方和接收方的特性,所以我们在使用channel时要防止死锁的发生。很明显,如果我们在一个线程内向同一个channel同时进行读取和发送的操作,就会导致死锁。
package main
import (
"fmt"
)
func main() {
c := make(chan int) //声明一个int类型的无缓冲通道
c <- 1
i := <- c
fmt.Printf("receive %d\n", i)
}
有缓存的channel
- 我们可以通过如下方式声明一个有缓存的channel。有缓存的channel区别在于只有当缓冲区被填满时,才会阻塞发送者,只有当缓冲区为空时才会阻塞接受者。
c := make(chan int, 10)
- 观察如下的例子:我们声明了一个容量为2的有缓冲的channel。开启一个协程,这个协程会向这个channel连续发送4个数据,然后休眠5s,接着再向channel发送2个数据。而主线程则会从这个channel中读取数据,每次读取前会先休眠1s。通过观察程序输出,我们可以发现,协程首先向channel发送了2个数据后(0、1),被阻塞,因为这时主线程在进行1s的休眠。主线程休眠结束后,从channel中读取了第一个数据0,之后继续休眠1s。channel此时的又有了缓冲,于是协程又向channel发送了第三个数据2,而后再次因为channel的缓冲区已满而休眠。依次类推,直到协程将4个数据发送完成之后,开始进行了5s的休眠。而当主线程从channel读完第4个数据(3)之后,当准备再从channel中读取第五个数据时,由于channel为空,主线程作为接受者被阻塞。直到协程的5s休眠结束,再次向channel中发送数据后,主线程读取到数据而不被阻塞。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 2) //声明一个int类型的有缓冲通道
go func() {
for i := 0; i < 4; i++ {
c <- i
fmt.Printf("send %d\n", i)
}
time.Sleep(5 * time.Second)
for i := 4; i < 6; i++ {
c <- i
fmt.Printf("send %d\n", i)
}
}()
for i := 0; i < 6; i++ {
time.Sleep(time.Second)
fmt.Printf("receive %d\n", <-c)
}
}
输出:
关闭一个channel
- 我们可以使用close关键字关闭一个channel,如下所示。关闭channel时我们要注意一些细节。
c := make(chan int, 2)
close(c)
- 关闭channel的操作原则上应该由发送者完成,因为如果仍然向一个已关闭的channel发送数据,会导致程序抛出panic。而如果由接受者关闭channel,则会遇到这个风险。
package main
func main() {
c := make(chan int, 2)
close(c)
c <- 1
}
- 从一个已关闭的channel中读取数据不会报错。只不过需要注意的是,接受者就不会被一个已关闭的channel的阻塞。而且接受者从关闭的channel中仍然可以读取出数据,只不过是这个channel的数据类型的默认值。我们可以通过指定接受状态位来观察接受的数据是否是从一个已关闭的channel所发送出来的数据。例如
j, ok := <-c
,则ok为false时,则代表channel已经被关闭。 - 如下图这个例子,在协程关闭channel之前,主线程仍然会被这个协程所阻塞,而且读取数据时,注意状态位是true。当协程关闭channel之后,主线程仍然可以从channel中读取出int的默认值0,只不过状态变量变为了false,而且不再被阻塞,直到循环结束。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 2)
go func() {
c <- 1
time.Sleep(time.Second)
c <- 2
time.Sleep(time.Second)
close(c)
}()
for i := 0; i < 6; i++ {
j, ok := <-c
fmt.Printf("receive: %d, status: %t\n", j, ok)
}
}
和select关键字及for循环配合使用
for range 语法
- 我们可以使用for循环,持续的从一个channel中接受数据,当channel为空时,for循环会被阻塞。当channel被关闭时,则会跳出for循环。
- 如下例子,协程向channel中循环发送数据,并在循环结束时关闭channel。主线程是使用for range语句从channel中读取数据,很明显可以观察到,当channel为空时,for循环会被阻塞,当channel为无缓冲的时候也是如此。当协程关闭channel后,主线程跳出了for循环。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 3; i++ {
c <-i
fmt.Printf("send %d\n", i)
time.Sleep(time.Second)
}
fmt.Println("ready close channel")
close(c)
}()
for i := range c {
fmt.Printf("receive %d\n", i)
}
fmt.Println("quit for loop")
}
select语法
- 使用select语句可以在多个可供选择的channel中读取任意一个数据执行。如果没有任何一个channel可以读取数据,则线程会被阻塞住,直到可以从某一个channel中读取数据为止。
- select语句不会循环如果需要循环读取,需要手动在select语句外加循环.
- 如下这个例子中,主线程使用select语句从c、c2任意一个channel中读取数据。两个协程分布向c,c2中发送数据,其中一个在1s后发送,另一个在2s后发送。可以看到主线程一开始无法从任何一个channel中读取到数据,处于阻塞状态。在1s时收到了c2的数据,然后就会继续往下运行。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
c2 := make(chan int)
go func() {
time.Sleep(2 * time.Second)
c <- 1
}()
go func() {
time.Sleep(1 * time.Second)
c2 <- 2
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
case i := <- c2:
fmt.Printf("receive from c2: %d\n", i)
}
}
- select语句还可以用于发送方。如下例子中,主线程将随机挑选一个仍有缓冲区channel发送数据,如果缓冲区已满,则这个channel的case语句将会被阻塞。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
c2 := make(chan int)
go func() {
fmt.Printf("receive from c: %d\n", <-c)
}()
go func() {
fmt.Printf("receive from c2: %d\n", <-c2)
}()
time.Sleep(time.Second)
select {
case c <- 1:
fmt.Printf("send c\n")
case c2 <- 1:
fmt.Printf("send c2\n")
}
}
- 注意close 一个channel也可以使select语句不再阻塞
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(2 * time.Second)
close(c)
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
}
}
输出:
select配合default使用
- 使用default关键字。使用select语句时,我们可以使用default关键字。和switch类似,这是一个默认的分支。如果所有的channel都没有准备好(例如对于发送者所有的channel都缓存已满,或对于接受者所有channel的缓存已空),则程序会进入default分支的逻辑。
- 这是刚刚的select例子,唯一不同的是我们在select语句中加入了一个default分支。运行后可以发现,主线程没有等待任何一个协程发送数据,而是直接进入了default的逻辑
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
c2 := make(chan int)
go func() {
time.Sleep(2 * time.Second)
c <- 1
}()
go func() {
time.Sleep(1 * time.Second)
c2 <- 2
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
case i := <- c2:
fmt.Printf("receive from c2: %d\n", i)
default:
fmt.Println("default")
}
}
输出:
使用time标准库中的channel
- golang的time标准库里提供了一些定时发送数据的channel,可以帮助我们实现一些功能。
- 例如利用
time.After()
函数配合select语句使用,可以实现超时的功能。本质上是time.After()
函数返回了一个channel并在我们设定的时间后向其发送一个数据。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(2 * time.Second)
c <- 1
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
case <-time.After(time.Second):
fmt.Println("timeout! ")
}
}
输出:
- time标准库中的
time.NewTicker()
函数返回一个带有channel的结构体,并定时向这个结构体中发送时间数据,以实现定时器的功能。
package main
import (
"fmt"
"time"
)
func main() {
c := time.NewTicker(time.Second)
for t := range c.C {
fmt.Printf("receive t :%s\n", t)
}
}
输出:
总结
- 通道(channel)则是用来传递数据的一个数据结构,除了传递基本类型的数据外还可以传递自定义的类型
- channel有带缓冲区和不带缓冲区(相当于缓冲区容量为0)两种类型。当缓冲区已满时会阻塞发送者,当缓冲区已空时会阻塞接受者。channel是线程安全的。
- 使用close关键字可以关闭channel。向已关闭的channel的发数据会panic。已关闭的channel中仍然可以读取数据,可以通过接受ok参数判断是否是从一个已关闭的channel中读取的数据。
- 使用for range语法可以从channel中循环读取数据,若channel为空,则循环会被阻塞,关闭channel会跳出循环。
- select case语句挑选一个能读取出数据(或发送数据)的channel继续执行。如果有多个channel满足条件,则挑选其中任意一个。如果所有的channel都被阻塞,则select语句会被阻塞。使用default关键字可以避免select语句被阻塞。关闭一个channel同样可以使select语句不再阻塞。
- time标准库中提供了
time.After()
方法,返回一个定时发送数据的channel,可以和select语句配合实现超时的逻辑。time标准库中还提供了time.NewTicker()
方法,返回一个带有channel的结构体,并定时向这个结构体中发送时间数据,可以实现定时器的功能。