敞开生长之旅!这是我参与「日新方案 2 月更文挑战」的第 1 天,点击查看活动详情
耐性和耐久胜过激烈和疯狂。
哈喽大家好,我是陈明勇,本文介绍的内容是 Go 并发模块的两个重要人物 → goroutine 与 channel。假如本文对你有协助,不妨点个赞,假如你是 Go 言语初学者,不妨点个关注,一起生长一起进步,假如本文有错误的当地,欢迎指出!
前语
Go 言语的 CSP 并发模型的完成包含两个主要组成部分:一个是 Goroutine,另一个是 channel。本文将会介绍它们的根本用法和留意事项。
Goroutine
Goroutine 是 Go 使用的根本履行单元,它是一种轻量的用户级线程,其底层是经过 coroutine(协程)去完成的并发。众所周知,协程是一种运转在用户态的用户线程,因而 Goroutine 也是被调度于 Go 程序运转时。
根本用法
语法:go + 函数/办法
经过 go 关键字 + 函数/办法 能够创立一个 Goroutine。
代码示例:
import (
"fmt"
"time"
)
func printGo() {
fmt.Println("签字函数")
}
type G struct {
}
func (g G) g() {
fmt.Println("办法")
}
func main() {
// 根据签字函数创立 goroutine
go printGo()
// 根据办法创立 goroutine
g := G{}
go g.g()
// 根据匿名函数创立 goroutine
go func() {
fmt.Println("匿名函数")
}()
// 根据闭包创立 goroutine
i := 0
go func() {
i++
fmt.Println("闭包")
}()
time.Sleep(time.Second) // 避免 main goroutine 完毕后,其创立的 goroutine 来不及运转,因而在此休眠 1 秒
}
履行结果:
闭包
签字函数
办法
匿名函数
当多个 Goroutine 存在时,它们的履行顺序是不固定的。因而每次打印的结果都不相同。
由代码可知,经过 go 关键字,咱们能够根据 签字函数 / 办法 创立 goroutine,也能够根据 匿名函数 / 闭包 创立 goroutine。
那么 Goroutine 是怎么退出的呢?正常状况下,只要 Goroutine 函数履行完毕,或许履行返回,意味着 Goroutine 的退出。假如 Goroutine 的函数或办法有返回值,在 Goroutine 退出时会将其忽略。
channel
channel 在 Go 并发模型中扮演者重要的人物。它能够用于完成 Goroutine 间的通讯,也能够用来完成 Goroutine 间的同步。
channel 的根本操作
channel 是一种复合数据类型,声明时需求指定 channel 里元素的类型。
声明语法:var ch chan string
经过上述代码声明一个元素类型为 string 的 channel,其只能存放 string 类型的元素。channel 是引用类型,必须初始化才能写入数据,经过 make 的方式初始化。
import (
"fmt"
)
func main() {
var ch chan string
ch = make(chan string, 1)
// 打印 chan 的地址
fmt.Println(ch)
// 向 ch 发送 "Go" 数据
ch <- "Go"
// 从 ch 中接纳数据
s := <-ch
fmt.Println(s) // Go
}
经过 ch <- xxx 能够向 channel 变量 ch 发送数据,经过 x := <- ch 能够从 channel 变量 ch 中接纳数据。
带缓冲 channel 与无缓冲 channel
假如初始化 channel 时,不指定容量时,则创立的是一个无缓冲的 channel:
ch := make(chan string)
无缓冲的 channel 的发送与接纳操作是同步的,在履行发送操作之后,对应 Goroutine 将会阻塞,直到有另一个 Goroutine 去履行接纳操作,反之亦然。假如将发送操作和履行操作放在同一个 Goroutine 下进行,会产生什么操作呢?看看下述代码:
import (
"fmt"
)
func main() {
ch := make(chan int)
// 发送数据
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
// 接纳数据
n := <-ch
fmt.Println(n)
}
程序运转之后,会在 ch <- 处得到 fatal error,提示一切的 Goroutine 处于休眠状态,也就是死锁了。为避免这种状况,咱们需求将 channel 的发送操作和接纳操作放到不同的 Goroutine 中履行。
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
// 发送数据
ch <- 1
}()
// 接纳数据
n := <-ch
fmt.Println(n) // 1
}
由上述例子能够得出结论:无缓冲 channel 的发送与接纳操作,一定要放在两个不同的 Goroutine 中进行,不然会产生 deadlock 形象。
假如指定容量,则创立的是一个带缓冲的 channel:
ch := make(chan string, 5)
有缓冲的 channel 与无缓冲的 chennel 有所区别,履行发送操作时,只要 channel 的缓冲区未满,Goroutine 不会挂起,直到缓冲区满时,再向 channel 履行发送操作,才会导致 Goroutine 挂起。代码示例:
func main() {
ch := make(chan int, 1)
// 发送数据
ch <- 1
ch <- 2 // fatal error: all goroutines are asleep - deadlock!
}
声明 channel 的只发送类型和只接纳类型
-
既能发送又能接纳的
channelch := make(chan int, 1)经过上述代码取得
channel变量,咱们能够对它履行发送与接纳的操作。 -
只接纳的
channelch := make(<-chan int, 1)经过上述代码取得
channel变量,咱们只能对它进行接纳操作。 -
只发送的
channelch := make(chan<- int, 1)经过上述代码取得
channel变量,咱们只能对它进行发送操作。
一般只发送 channel 类型和只接纳 channel 类型,会被用作函数的参数类型或返回值:
func send(ch chan<- int) {
ch <- 1
}
func recv(ch <-chan int) {
<-ch
}
channel 的封闭
经过内置函 close(c chan<- Type),能够对 channel 进行封闭。
- 在发送端封闭
channel
在channel封闭之后,将不能对channel履行发送操作,不然会产生panic,提示channel已封闭。func main() { ch := make(chan int, 5) ch <- 1 close(ch) ch <- 2 // panic: send on closed channel } - 管道
channel之后,依旧能够对channel履行接纳操作,假如存在缓冲区的状况下,将会读取缓冲区的数据,假如缓冲区为空,则获取到的值为channel对应类型的零值。import "fmt" func main() { ch := make(chan int, 5) ch <- 1 close(ch) fmt.Println(<-ch) // 1 n, ok := <-ch fmt.Println(n) // 0 fmt.Println(ok) // false } - 假如经过 for-range 遍历
channel时,中途封闭channel则会导致for-range循环完毕。
小结
本文首先介绍了 Goroutine的创立方式以及其退出的机遇是什么。
其次介绍了怎么创立 channel 类型变量的有缓冲与无缓冲的创立方式。需求留意的是,无缓冲的 channel 发送与接纳操作,需求在两个不同的 Goroutine 中履行,不然会发送 error。
接下来介绍怎么定义只发送和只接纳的 channel 类型。一般只发送 channel 类型和只接纳 channel 类型,会被用作函数的参数类型或返回值。
最后介绍了怎么封闭 channel,以及封闭之后的一些留意事项。
