【Go笔记】第 16 章 goroutine 和 channel

9/27/2022 韩顺平尚硅谷golang笔记

# 16.1 goroutine-看一个需求

🏷 需求:要求统计 1-9000000000 的数字中,哪些是素数? 🏷 分析思路:

  1. 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
  2. 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到 goroutine.【速度提高 4 倍】

# 16.2 goroutine-基本介绍

# 16.2.1 进程和线程介绍

  1. 进程就是程序程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
  3. 一个进程可以创建核销毁多个线程,同一个进程中的多个线程可以并发执行。
  4. 一个程序至少有一个进程,一个进程至少有一个线程

# 16.2.2 程序、进程和线程的关系示意图

# 16.2.3 并发和并行

🏷 并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行
  3. 示意图:

🏷 小结 并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。

并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,同时也有10个线程在执行,这就是并行

# 16.2.4 Go 协程和 Go 主线程

🏷 Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]

🏷 Go 协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

🏷 一个示意图

# 16.3 goroutine-快速入门

# 16.3.1 案例说明

🏷 请编写一个程序,完成如下功能:

  1. 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
  2. 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
  3. 要求主线程和 goroutine 同时执行.
  4. 画出 主线程和协程执行流程图

🏷 代码实现


package main  
  
import (  
   "fmt"  
   "strconv"   "time")  
  
//编写一个函数,每隔1秒输出"he11o,wor1d”  
func test() {  
   for i := 1; i <= 10; i++ {  
      fmt.Println("tesst (hello,world " + strconv.Itoa(i))  
      time.Sleep(time.Second)  
   }  
}  
  
func main() {  
   go test() //1 开启了一个协程  
   for i := 1; i <= 10; i++ {  
      fmt.Println("main()hello,golang" + strconv.Itoa(i))  
      time.Sleep(time.Second)  
   }  
}

//输出的效果说明, main 这个主线程和 test 协程同时执行.
/**
main()hello,golang1
tesst (hello,world 1
main()hello,golang2
tesst (hello,world 2
tesst (hello,world 3
main()hello,golang3
main()hello,golang4
tesst (hello,world 4
tesst (hello,world 5
main()hello,golang5
tesst (hello,world 6
main()hello,golang6
main()hello,golang7
tesst (hello,world 7
main()hello,golang8
tesst (hello,world 8
tesst (hello,world 9
main()hello,golang9
main()hello,golang10
tesst (hello,world 10
**/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

🏷 主线程和协程执行流程图

# 16.3.2 快速入门小结

  1. 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang 的协程机制是重要的特点,可以轻松的 开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了

# 16.4 goroutine 的调度模型

# 16.4.1 MPG 模式基本介绍

# 16.4.2 MPG 模式运行的状态 1

# 16.4.3 MPG 模式运行的状态 2

# 16.5 设置 Golang 运行的 cpu 数

🏷 介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目


package main  
  
import (  
   "fmt"  
   "runtime")  
  
func main() {  
   //获取当前系统cpu的数量  
   num := runtime.NumCPU()  
   //我这里设置num-1的cpu运行go程序  
   runtime.GOMAXPROCS(num)  
   fmt.Println("num=", num)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. g01.8后,默认让程序运行在多个核上,可以不用设置了
  2. go1.8前,还是要设置一下,可以更高效的利益cpu

# 16.6 channel(管道)-看个需求

需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。 要求使用 goroutine 完成 🏷 分析思路:

  1. 使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题.
  2. 这里就提出了不同 goroutine 如何通信的问题

🏷 代码实现

  1. 使用 goroutine 来完成(看看使用 gorotine 并发完成会出现什么问题? 然后我们会去解决)
  2. 在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一个参数 -race 即可 [示意图]
  3. 代码实现:

package main  
  
import (  
   "fmt"  
   "time")  
  
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。  
// 最后显示出来。要求使用 goroutine 完成  
// 思路  
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中.  
// 2. 我们启动的协程多个,统计的将结果放入到 map 中  
  
// 3. map 应该做出一个全局的.  
var (  
   myMap = make(map[int]int, 10)  
)  
  
// test 函数就是计算 n!, 让将这个结果放入到 myMap  
func test(n int) {  
   res := 1  
   for i := 1; i <= n; i++ {  
      res *= i  
   }  
   //这里我们将 res 放入到 myMap   myMap[n] = res //concurrent map writes?  
}  
func main() {  
   // 我们这里开启多个协程完成这个任务[200 个]  
   for i := 1; i <= 200; i++ {  
      go test(i)  
   }  
  
   //休眠 10 秒钟【第二个问题 】  
   time.Sleep(time.Second * 10)  
   //这里我们输出结果,变量这个结果  
   for i, v := range myMap {  
      fmt.Printf("map[%d]=%d\n", i, v)  
   }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  1. 示意图:

# 16.6.1 不同 goroutine 之间如何通讯

  1. 全局变量的互斥锁
  2. 使用管道 channel 来解决

# 16.6.2 使用全局变量加锁同步改进程序

🏷 因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes 🏷 解决方案:加入互斥锁 🏷 我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i) 🏷 代码改进


package main  
  
import (  
   "fmt"  
   "sync"   "time")  
  
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。  
// 最后显示出来。要求使用 goroutine 完成  
// 思路  
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中.  
// 2. 我们启动的协程多个,统计的将结果放入到 map 中  
  
// 3. map 应该做出一个全局的.  
var (  
   myMap = make(map[int]int, 10)  
   //声明一个全局的互斥锁  
   //1ock是一个全局的互斥锁  
   //sync是包:synchornized同步  
   //Mutex:是互斥  
   lock sync.Mutex  
)  
  
// test 函数就是计算 n!, 让将这个结果放入到 myMap  
func test(n int) {  
   res := 1  
   for i := 1; i <= n; i++ {  
      res *= i  
   }  
   //这里我们将 res 放入到 myMap   //加锁  
   lock.Lock()  
   myMap[n] = res //concurrent map writes?  
   //解锁  
   lock.Unlock()  
}  
  
func main() {  
   // 我们这里开启多个协程完成这个任务[200 个]  
   for i := 1; i <= 200; i++ {  
      go test(i)  
   }  
  
   //休眠 10 秒钟【第二个问题 】  
   time.Sleep(time.Second * 10)  
   lock.Lock()  
   //这里我们输出结果,变量这个结果  
   for i, v := range myMap {  
      fmt.Printf("map[%d]=%d\n", i, v)  
   }  
   lock.Unlock()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 16.6.3 为什么需要 channel

  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作 状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 上面种种分析都在呼唤一个新的通讯机制-channel

# 16.6.4 channel 的基本介绍

  1. channle 本质就是一个数据结构-队列【示意图】
  2. 数据是先进先出【FIFO : first in first out】
  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
  5. 示意图:

# 16.6.5 定义/声明 channel

🏷 var 变量名 chan 数据类型 🏷 举例:

var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan Person
var perChan2 chan *Person
...
1
2
3
4
5

🏷 说明 channel 是引用类型 channel 必须初始化才能写入数据, 即 make 后才能使用 管道是有类型的,intChan 只能写入 整数 int

# 16.6.6 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项


package main  
  
import (  
   "fmt"  
)  
  
func main() {  
   //演示一下管道的使用  
   //1. 创建一个可以存放 3 个 int 类型的管道  
   var intChan chan int  
   intChan = make(chan int, 3)  
   //2. 看看 intChan 是什么  
   fmt.Printf("intChan 的值=%v intChan 本身的地址=%p\n", intChan, &intChan)  
   //3. 向管道写入数据  
   intChan <- 10  
   num := 211  
   intChan <- num  
   intChan <- 50  
   // intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量  
   //4. 看看管道的长度和 cap(容量)  
   fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3  
  
   //5. 从管道中读取数据  
   var num2 int  
   num2 = <-intChan  
   fmt.Println("num2=", num2)  
   fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3  
   //6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock   num3 := <-intChan  
   num4 := <-intChan  
   num5 := <-intChan  
   fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 16.6.7 channel 使用的注意事项

  1. channel 中只能存放指定的数据类型
  2. channle 的数据放满后,就不能再放入了
  3. 如果从 channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

# 16.6.8 读写 channel 案例演示

  1. 创建一个intChan,最多可以存放3个int,演示存3数据到intChan,然后再取出这三个int

package main  
  
import (  
   "fmt"  
)  
  
func main() {  
   var intchan chan int  
   intchan = make(chan int, 3)  
   intchan <- 10  
   intchan <- 20  
   intchan <- 10  
   //因为intchan的容量为3,再存放会报告dead1ock  
   //intchan <-50   num1 := <-intchan  
   num2 := <-intchan  
   num3 := <-intchan  
   //因为intchan这时已经没有数据了,再取就会报告deadLock  
   //num4 :=<-intchan   fmt.Printf("numl=%v num2=%v num3=%v", num1, num2, num3)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. 创建一个mapChan,最多可以存放10个map[string]string的key-val,演示写入和读取

package main  
  
func main() {  
   var mapchan chan map[string]string  
   mapchan = make(chan map[string]string, 10)  
   m1 := make(map[string]string, 20)  
   m1["city1"] = "北京"  
   m1["c1ty2"] = "天津"  
   m2 := make(map[string]string, 20)  
   m2["hero1"] = "宋江"  
   m2["hero2"] = "武松"  
   //...  
   mapchan <- m1  
   mapchan <- m2  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和读取的用法
package main  
  
import "fmt"  
  
type Cat struct {  
   Name  string  
   Age   int  
   Color string  
   Hobby string  
}  
    
func main() {  
   var catChan chan Cat  
   catChan = make(chan Cat, 10)  
  
   cat1 := Cat{Name: "tom", Age: 18}  
   cat2 := Cat{Name: "tom~", Age: 180}  
   catChan <- cat1  
   catChan <- cat2  
   //取出  
   cat11 := <-catChan  
   cat22 := <-catChan  
   fmt.Println(cat11, cat22)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 创建一个catChan:2,最多可以存放10个 *Cat 变量,演示写入和读取的用法

package main  
  
import "fmt"  
  
type Cat struct {  
   Name  string  
   Age   int  
   Color string  
   Hobby string  
}  
    
func main() {  
   var catchan chan *Cat  
   catchan = make(chan *Cat, 10)  
   cat1 := Cat{Name: "tom", Age: 18}  
   cat2 := Cat{Name: "tom~", Age: 180}  
   catchan <- &cat1  
   catchan <- &cat2  
   //取出  
   cat11 := <-catchan  
   cat22 := <-catchan  
   fmt.Println(cat11, cat22)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 创建一个allChan,, 最多可以存放10个任意数据类型变量,演示写入和读取的用法

package main  
  
import "fmt"  
  
type Cat struct {  
   Name  string  
   Age   int  
   Color string  
   Hobby string  
}  
  
func main() {  
   var allchan chan interface{}  
   allchan = make(chan interface{}, 10)  
   
   cat1 := Cat{Name: "tom", Age: 18}  
   cat2 := Cat{Name: "tom", Age: 180}  
   
   allchan <- cat1  
   allchan <- cat2  
   allchan <- 10  
   allchan <- "jack"  
   
   //取出  
   catil := <-allchan  
   cat22 := <-allchan  
   v1 := <-allchan  
   v2 := <-allchan  
   fmt.Println(catil, cat22, v1, v2)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

6)看下面的代码会输出什么?


package main  
  
import "fmt"  
  
type Cat struct {  
   Name  string  
   Age   int  
   Color string  
   Hobby string  
}  
  

func main() {  
   var allchan chan interface{}  
   allchan = make(chan interface{}, 10)  
   
   cat1 := Cat{Name: "tom", Age: 18}  
   cat2 := Cat{Name: "tom~", Age: 180}  
   
   allchan <- cat1  
   allchan <- cat2  
   allchan <- 10  
   allchan <- "jack"  
   
   //取出  
   //cat11 := <-allchan  
   //fmt.Println(cat11.Name) //报错  
  
   newcat := <-allchan //从管道中取出的cat是什么?  
   fmt.Printf("newcat=%T,newcat=%v\n", newcat, newcat)  
   //下面的写法是错误的!编译不通过  
   //fmt.Printf("newcat.Name=%v",newcat.Name)  
   //使用类型断言  
   a := newcat.(Cat)  
   fmt.Printf("newcat.Name=%v", a.Name)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 16.7 管道的课后练习题

读写channel课堂练习/课后练习 说明:请完成如下案例 1)创建一个Person结构体[Name,Age,Address] 2)使用rand方法配合随机创建10个Person实例,并放入到channel中 3)遍历channel,将各个Person实例的信息显示在终端.

# 16.8 channel 的遍历和关闭

# 16.8.1 channel 的关闭

使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据 案例演示:


package main  
  
import "fmt"  
  
func main() {  
   intchan := make(chan int, 3)  
   intchan <- 100  
   intchan <- 200  
   close(intchan) //close  
   //这是不能够再写入数到channel  
   //intchan<-300   fmt.Println("okook~")  
   //当管道关闭后,读取数据是可以的  
   n1 := <-intchan  
   fmt.Println("ni=", n1)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 16.8.2 channel 的遍历

channel 支持 for--range 的方式进行遍历,请注意两个细节

  1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

# 16.8.3 channel 遍历和关闭的案例演示

看代码演示:


package main  
  
import "fmt"  
  
func main() {  
   //遍历管道  
   intchan2 := make(chan int, 100)  
   for i := 0; i < 100; i++ {  
      intchan2 <- i * 2 //放入18g个数据到管道  
   }  
   //遍历管道不能使用普通的for循环  
   //for i:=0;i<len(intchan2);i++{}  
   //在遍历时,如果channe1没有关闭,则会出现deadlock的错误  
   //在遍历时,如果channe1已经关闭,则会正常遍历效据,遍历完后,就会退出遍历  
   close(intchan2)  
   for v := range intchan2 {  
      fmt.Println("v=", v)  
   }  
  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 16.8.4 应用实例 1

请完成goroutine和channelf协同工作的案例,具体要求: 1)开启一个writeData协程,向管道intChan中写入50个整数 2)开启一个readData协程,从管道intChan中读取writeData写入的数据。 3)注意:writeData和readDate?操作的是同一个管道 4)主线程需要等待writeData和readDate协程都完成工作才能退出【管道】

🏷 思路分析:

🏷 代码的实现:


package main  
  
import (  
   "fmt"  
   _ "time"  
)  
  
//write Data  
func writeData(intChan chan int) {  
   for i := 1; i <= 50; i++ {  
      //放入数据  
      intChan <- i  
      fmt.Println("writeData ", i)  
      //time.Sleep(time.Second)  
   }  
   close(intChan) //关闭  
}  
  
//read data  
func readData(intChan chan int, exitChan chan bool) {  
   for {  
      v, ok := <-intChan  
      if !ok {  
         break  
      }  
      //time.Sleep(time.Second)  
      fmt.Printf("readData 读到数据=%v\n", v)  
   }  
   //readData 读取完数据后,即任务完成  
   exitChan <- true  
   close(exitChan)  
}  
func main() {  
   //创建两个管道  
   intChan := make(chan int, 50)  
   exitChan := make(chan bool, 1)  
   go writeData(intChan)  
   go readData(intChan, exitChan)  
   //time.Sleep(time.Second * 10)  
   for {  
      _, ok := <-exitChan  
      if !ok {  
         break  
      }  
   }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# 16.8.5 应用实例 2-阻塞

# 16.8.6 应用实例 3

🏷 需求: 要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine 和 channel 的知识后,就可以完成了 [测试数据: 80000]

🏷 分析思路: 传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。 使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。

🏷 画出分析思路

🏷 代码实现


package main  
  
import (  
   "fmt"  
   "time")  
  
//向 intChan 放入 1-8000 个数  
func putNum(intChan chan int) {  
   for i := 1; i <= 8000; i++ {  
      intChan <- i  
   }  
   //关闭 intChan   close(intChan)  
}  
  
// 从 intChan 取出数据,并判断是否为素数,如果是,就  
// //放入到 primeChanfunc primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {  
   //使用 for 循环  
   // var num int  
   var flag bool //  
   for {  
      time.Sleep(time.Millisecond * 10)  
      num, ok := <-intChan  
      if !ok { //intChan 取不到..  
         break  
      }  
      flag = true //假设是素数  
      //判断 num 是不是素数  
      for i := 2; i < num; i++ {  
         if num%i == 0 { //说明该 num 不是素数  
            flag = false  
            break  
         }  
      }      if flag {  
         //将这个数就放入到 primeChan         primeChan <- num  
      }  
   }   fmt.Println("有一个 primeNum 协程因为取不到数据,退出")  
   //这里我们还不能关闭 primeChan   //向 exitChan 写入 true   exitChan <- true  
}  
func main() {  
   intChan := make(chan int, 1000)  
   primeChan := make(chan int, 2000) //放入结果  
   //标识退出的管道  
   exitChan := make(chan bool, 4) // 4 个  
   //开启一个协程,向 intChan 放入 1-8000 个数  
   go putNum(intChan)  
   //开启 4 个协程,从 intChan 取出数据,并判断是否为素数,如果是,就  
   //放入到 primeChan   for i := 0; i < 4; i++ {  
      go primeNum(intChan, primeChan, exitChan)  
   }  
   //这里我们主线程,进行处理  
   //直接  
   go func() {  
      for i := 0; i < 4; i++ {  
         <-exitChan  
      }  
      //当我们从 exitChan 取出了 4 个结果,就可以放心的关闭 prprimeChan      close(primeChan)  
   }()  
   //遍历我们的 primeChan ,把结果取出  
   for {  
      res, ok := <-primeChan  
      if !ok {  
         break  
      }  
      //将结果输出  
      fmt.Printf("素数=%d\n", res)  
   }  
   fmt.Println("main 线程退出")  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

结论:使用 go 协程后,执行的速度,比普通方法提高至少 4 倍

# 16.9 channel 使用细节和注意事项

  1. channel 可以声明为只读,或者只写性质 【案例演示】

package main  
  
import "fmt"  
  
func main() {  
   //管道可以声明为只读或者只写  
   //1.在默认情况下下,管道是双向  
   //var chan1 chan int//可读可写  
   //2声明为只写  
   var chan2 chan<- int  
   chan2 = make(chan int, 3)  
   chan2 <- 20  
   //num :=<-chan2 //error  
   fmt.Println("chan2=", chan2)  
   //3.声明为只读  
   var chan3 <-chan int  
   num2 := <-chan3  
   //chan3<-30 //err  
   fmt.Println("num2", num2)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. channel 只读和只写的最佳实践案例

  2. 使用 select 可以解决从管道取数据的阻塞问题


package main  
  
import (  
   "fmt"  
   "time")  
  
func main() {  
   //使用 select 可以解决从管道取数据的阻塞问题  
   //1.定义一个管道 10 个数据 int   intChan := make(chan int, 10)  
   for i := 0; i < 10; i++ {  
      intChan <- i  
   }  
   //2.定义一个管道 5 个数据 string   stringChan := make(chan string, 5)  
   for i := 0; i < 5; i++ {  
      stringChan <- "hello" + fmt.Sprintf("%d", i)  
   }  
   //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock   //问题,在实际开发中,可能我们不好确定什么关闭该管道.  
   //可以使用 select 方式可以解决  
   //label:  
   for {  
      select {  
      //注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock      //,会自动到下一个 case 匹配  
      case v := <-intChan:  
         fmt.Printf("从 intChan 读取的数据%d\n", v)  
         time.Sleep(time.Second)  
      case v := <-stringChan:  
         fmt.Printf("从 stringChan 读取的数据%s\n", v)  
         time.Sleep(time.Second)  
      default:  
         fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")  
         time.Sleep(time.Second)  
         return  
         //break label  
      }  
   }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  1. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题

package main  
  
import (  
   "fmt"  
   "time")  
  
//函数  
func sayHello() {  
   for i := 0; i < 10; i++ {  
      time.Sleep(time.Second)  
      fmt.Println("hello,world")  
   }  
}  
  
//函数  
func test() {  
   //这里我们可以使用 defer + recover   defer func() {  
      //捕获 test 抛出的 panic      if err := recover(); err != nil {  
         fmt.Println("test() 发生错误", err)  
      }  
   }()  
   //定义了一个 map   var myMap map[int]string  
   myMap[0] = "golang" //error  
}  
func main() {  
   go sayHello()  
   go test()  
   for i := 0; i < 10; i++ {  
      fmt.Println("main() ok=", i)  
      time.Sleep(time.Second)  
   }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33