敞开生长之旅!这是我参与「日新方案 2 月更文挑战」的第 1 天,点击查看活动详情

耐性和耐久胜过激烈和疯狂。

哈喽大家好,我是陈明勇,本文介绍的内容是 Go 并发模块的两个重要人物 → goroutinechannel。假如本文对你有协助,不妨点个赞,假如你是 Go 言语初学者,不妨点个关注,一起生长一起进步,假如本文有错误的当地,欢迎指出!

前语

Go 言语的 CSP 并发模型的完成包含两个主要组成部分:一个是 Goroutine,另一个是 channel。本文将会介绍它们的根本用法和留意事项。

Goroutine

GoroutineGo 使用的根本履行单元,它是一种轻量的用户级线程,其底层是经过 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

经过上述代码声明一个元素类型为 stringchannel,其只能存放 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 的只发送类型和只接纳类型

  • 既能发送又能接纳的 channel

    ch := make(chan int, 1)
    

    经过上述代码取得 channel 变量,咱们能够对它履行发送与接纳的操作。

  • 只接纳的 channel

    ch := make(<-chan int, 1)
    

    经过上述代码取得 channel 变量,咱们只能对它进行接纳操作。

  • 只发送的 channel

    ch := 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,以及封闭之后的一些留意事项。