关注【索引目录】服务号,更多精彩内容等你来探索!
前言
Go 的 CSP 并发模型的实现主要由两个组件组成:一个是 Goroutine,一个是 Channel。本文将介绍它们的基本使用方法以及注意事项。
Goroutine
Goroutine 是 Go 应用程序的基本执行单元。它是一个轻量级的用户级线程,其并发底层实现基于协程。众所周知,协程是运行在用户态的用户线程,因此 Goroutine 也由 Go 运行时进行调度。
基本用法
语法: go+函数/方法
你可以使用go关键字加上函数/方法来创建一个 Goroutine。
示例代码:
import (
"fmt"
"time"
)
func printGo() {
fmt.Println("Named function")
}
type G struct {
}
func (g G) g() {
fmt.Println("Method")
}
func main() {
// Create goroutine from named function
go printGo()
// Create goroutine from method
g := G{}
go g.g()
// Create goroutine from anonymous function
go func() {
fmt.Println("Anonymous function")
}()
// Create goroutine from closure
i := 0
go func() {
i++
fmt.Println("Closure")
}()
time.Sleep(time.Second) // Prevent main goroutine from ending before the created goroutines have a chance to run; hence sleep for 1 second
}
执行结果:
Named function
Method
Anonymous function
当存在多个 Goroutine 时,它们的执行顺序是不固定的,所以每次打印的结果都会不一样。
从代码中可以看到,通过使用go关键字,我们可以基于命名函数或方法创建 Goroutine,也可以基于匿名函数或闭包创建 Goroutine。
那么,Goroutine 是如何退出的呢?通常,Goroutine 的函数执行完毕或返回后,它就会退出。如果 Goroutine 中的函数或方法有返回值,则 Goroutine 退出时会忽略该返回值。
渠道
通道在 Go 的并发模型中扮演着重要的角色。它们可以用于 Goroutine 之间的通信,也可以用于 Goroutine 之间的同步。
通道基本操作
通道是一种复合数据类型,在声明它时,需要指定它将存储的元素类型。
声明语法: var ch chan string
上述代码声明了一个元素类型为 的通道string,这意味着它只能存储string值。通道是引用类型,必须先初始化才能写入数据。它的初始化方法是使用make。
import (
"fmt"
)
func main() {
var ch chan string
ch = make(chan string, 1)
// Print address of channel
fmt.Println(ch)
// Send "Go" into ch
ch <- "Go"
// Receive data from ch
s := <-ch
fmt.Println(s) // Go
}
ch您可以使用将数据发送到通道变量ch <- xxx并使用 从中接收数据x := <-ch。
缓冲通道与非缓冲通道
如果在初始化通道时未指定容量,则会创建无缓冲通道:
ch := make(chan string)
在无缓冲通道中,发送和接收操作是同步的。执行发送操作后,相应的 Goroutine 会阻塞,直到另一个 Goroutine 执行接收操作,反之亦然。如果将发送和接收操作放在同一个 Goroutine 中会发生什么?我们来看以下代码:
import (
"fmt"
)
func main() {
ch := make(chan int)
// Send data
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
// Receive data
n := <-ch
fmt.Println(n)
}
程序运行时,会抛出一个致命错误,ch <-指出所有 Goroutine 都处于睡眠状态——换句话说,发生了死锁。为了避免这种情况,我们需要将发送和接收操作放在不同的 Goroutine 中。
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
// Send data
ch <- 1
}()
// Receive data
n := <-ch
fmt.Println(n) // 1
}
从上面的例子我们可以得出,对于无缓冲通道,发送和接收操作必须在两个不同的 Goroutine 中执行,否则就会发生死锁。
如果指定容量,则会创建一个缓冲通道:
ch := make(chan string, 5)
缓冲通道与无缓冲通道不同:在执行发送操作时,只要通道的缓冲区未满,Goroutine 就不会被挂起。只有当缓冲区已满时,向通道发送数据才会导致 Goroutine 被挂起。示例代码:
func main() {
ch := make(chan int, 1)
// Send data
ch <- 1
ch <- 2 // fatal error: all goroutines are asleep - deadlock!
}
声明仅发送和仅接收通道
可以发送和接收的通道
ch := make(chan int, 1)
通过上述代码,我们得到一个通道变量,可以在其上执行发送和接收操作。
仅接收通道
ch := make(<-chan int, 1)
通过上面的代码,我们得到了一个只能执行接收操作的通道变量。
仅发送频道
ch := make(chan<- int, 1)
通过上面的代码,我们得到了一个只能执行发送操作的通道变量。
通常,仅发送和仅接收通道类型用作函数参数类型或返回值:
func send(ch chan<- int) {
ch <- 1
}
func recv(ch <-chan int) {
<-ch
}
关闭通道
您可以使用内置函数关闭通道close(c chan<- Type)。
在发送端关闭通道
通道关闭后,不能再对其进行发送操作,否则将引发恐慌,提示该通道已经关闭。
func main() {
ch := make(chan int, 5)
ch <- 1
close(ch)
ch <- 2 // panic: send on closed 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,如果在迭代过程中关闭通道,则for-range循环将结束。
概括
本文首先介绍了如何创建Goroutines以及Goroutines的退出条件。
然后,它描述了如何创建缓冲和非缓冲通道变量。需要注意的是,对于非缓冲通道,发送和接收操作必须在两个不同的 Goroutine 中执行,否则将发生错误。
接下来,我们讲解了如何定义仅发送和仅接收的通道类型。这些类型通常用作函数的参数类型或返回值。
关注【索引目录】服务号,更多精彩内容等你来探索!

