大数跨境

理解 Go 的 CSP 模型:Goroutines 和 Channels

理解 Go 的 CSP 模型:Goroutines 和 Channels 索引目录
2025-08-11
1
导读:关注【索引目录】服务号,更多精彩内容等你来探索!前言Go 的 CSP 并发模型的实现主要由两个组件组成:一个是 Goroutine,一个是 Channel。

关注【索引目录】服务号,更多精彩内容等你来探索!

前言

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 中执行,否则将发生错误。

接下来,我们讲解了如何定义仅发送和仅接收的通道类型。这些类型通常用作函数的参数类型或返回值。


关注【索引目录】服务号,更多精彩内容等你来探索!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444