go语言系列-从Goroutine到Channel
golang语言的核心特色
goroutine
基本介绍
进程和线程介绍
-
进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
-
线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
-
一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
-
一个程序至少有一个进程,一个进程至少有一个线程
程序、进程和线程的关系示意图
并发和并行
-
多线程程序在单核上运行,就是并发
-
多线程程序在多核上运行,就是并行
并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
go协程和go主线程
go主线程(有程序员直接称为线程/也可以理解成进程):一个go线程上,可以起多个协程,可以这样理解:协程是轻量级的线程【编译器做优化】
go协程的特点
1) 有独立的栈空间
2) 共享程序堆空间
3) 调度由用户控制
4) 协程是轻量级的线程
快速入门
案例说明
编写一个程序,完成如下功能:
1) 在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔一秒输出“hello,world”
2) 在主线程中也每隔一秒输出“hello,world”,输出10次后,退出程序
3) 要求主线程和goroutine同时执行
画出主线程和协程执行流程图
import ( "fmt" "strconv" "time" ) //编写一个函数/每隔一秒输出"hello,world" func test() { for i := 1; i <= 10; i++ { fmt.println("test() hello,world" + strconv.itoa(i)) time.sleep(time.second) } } func main() { go test() //开启了一个协程 for i := 1; i <= 10; i++ { fmt.println("main() hello,world" + strconv.itoa(i)) time.sleep(time.second) } } //main() hello,world1 //main主线程和test协程同时执行 //test() hello,world1 //main() hello,world2 //test() hello,world2 //......
小结
-
主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源
-
协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
-
go的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就突显了go在并发上的优势了
goroutine的调度模型
-
m:操作系统的主线程(是物理线程)
-
p:协程执行需要的上下文
-
g:协程
mpg模式运行的状态 -1
-
当前程序有三个m,如果三个m都在一个cpu上运行,就是并发,如果在不同的cpu上运行就是并行
-
m1,m2,m3正在执行一个g,m1的协程队列有三个,m2的协程队列有三个,m3的协程队列有两个
-
从上图可以看到:go的协程是轻量级的线程,是逻辑态的,go可以容易的起上万个协程
-
其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu
mpg模式运行的状态 - 2
-
分成两个部分来看
-
原来的情况是mo主线程正在执行go协程,另外有三个协程在队列等待
-
如果go协程阻塞,比如读取文件或者数据库等
-
这时就会创建m1主线程(也可能是从已有的线程池中取出m1),并且将等待的3个协程挂到m1下开始执行,m0的主线程下的go仍然执行文件io的读写
-
这样的mpg调度模式,可以既让go执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行
-
等到go不阻塞了,m0会被放到空闲的主线程继续执行(从已有的线程池中取),同时go又会被唤醒
设置go运行的cpu数
为了充分利用多cpu的优势,在go程序中,设置运行的cpu数目
import ( "fmt" "runtime" ) func main() { //获取当前系统cpu的数目 num := runtime.numcpu() //这里设置num - 1的cpu运行go程序 runtime.gomaxprocs(num - 1) fmt.println("num = ", num) } go1.8后,默认让程序运行在多核上,可以不用设置 go1.8前,还是要设置一下,可以更高效的利用cpu
channel(管道)
看个需求
需求:现在要计算1 - 200 的各个数的阶乘,并且把各个数的阶乘放入到map中,最后显示出来
要求:使用goroutine
分析思路
1) 使用goroutine来完成,效率高,但是会出现并发/并行安全问题
2) 这里就提出了不同goroutine如何通信的问题
代码区
1) 使用goroutine来完成(看看使用goroutine并发完成会出现什么问题?然后再去解决)
2) 在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一个参数 - race 即可
示意图
import ( "fmt" "time" ) //思路 //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) } } //fatal error: concurrent map writes // //goroutine 55 [running]: //runtime.throw(0x4d6d6d, 0x15) // e:/go/go/src/runtime/panic.go:774 +0x79 fp=0xc0000eff60 sp=0xc0000eff30 pc=0x42d229 //runtime.mapassign_fast64(0x4b6240, 0xc00005c330, 0x31, 0x0) // e:/go/go/src/runtime/map_fast64.go:101 +0x357 fp=0xc0000effa0 sp=0xc0000eff60 pc=0x410167 //main.test(0x31) // e:/gostudent/src/2020-04-06/main.go:21 +0x6b fp=0xc0000effd8 sp=0xc0000effa0 pc=0x49c72b //runtime.goexit() // e:/go/go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc0000effe0 sp=0xc0000effd8 pc=0x4556a1 //created by main.main // e:/gostudent/src/2020-04-06/main.go:26 +0x5f // //goroutine 1 [runnable]: //time.sleep(0x2540be400) // e:/go/go/src/runtime/time.go:84 +0x248 //main.main() // e:/gostudent/src/2020-04-06/main.go:29 +0x82
不同goroutine之间如何通讯
-
全局变量的互斥锁
-
使用管道channel来解决
使用全局变量加锁同步改进程序
因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
解决方案:加入互斥锁
数的阶乘很大,结果会越界,可以将求阶乘改成sum += uint64(i)
代码区改进
package main import ( "fmt" "sync" "time" ) //思路 //1. 编写一个函数,计算各个数的阶乘,并放入到map中 //2. 启动的协程多个,统计的结果放入到map中 //3. map应该做出一个全局的 var ( mymap = make(map[uint]uint,10) //声明一个全局的互斥锁 //lock 是一个全局的互斥锁 //sync 是包:synchornized 同步 //mutex :是互斥 lock sync.mutex ) //test函数就是计算n!,将这个结果放入到mymap func test(n uint) { var res uint = 1 var i uint = 1 for ; i <= n; i++ { res *= i } //这里将res 放入到mymap //加锁 lock.lock() mymap[n] = res // concurrent map writes? //解锁 lock.unlock() } func main() { //这里开启多个协程完成这个任务[200个] var i uint = 1 for ; 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() } 需求注意的是:uint64最大到20的阶乘,大整数可以使用math/big 来进行 实例:https://blog.csdn.net/hudmhacker/article/details/90081630
为什么需要channel
-
前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
-
主线程在等待所有gorountine全部完成的时间很难确定,这里设置了10秒,仅仅是估算
-
如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
-
通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
-
上面种种分析都在呼唤一个新的通讯机制 - channel
channel的基本介绍
-
channel本质就是一个数据结构 - 队列
-
数据是先进先出【fifo :first int first out】
-
线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
-
channel有类型的,一个string的channel只能存放string类型数据
定义/声明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) channel是引用类型
2) channel必须初始化才能写入数据,即make后才能使用
3) 管道是有类型的,intchan只能写入整数int
管道的初始化、写入数据到管道、从管道读取数据
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 <- 99 //当给管道写入数据时,不能超过其容量 //4. 看看管道的长度和cap(容量) fmt.printf("channel len = %v cap = %v \n", len(intchan), cap(intchan)) //5. 从管道中读取数据 var num2 int num2 = <- intchan fmt.println("num2 = ", num2) fmt.printf("channel len = %v cap = %v \n", len(intchan), cap(intchan)) //6. 在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报告deadlock num3 := <- intchan num4 := <- intchan num5 := <- intchan fmt.printf("num3 = %v num4 = %v num5 = %v ", num3, num4, num5) } //fatal error: all goroutines are asleep - deadlock! //intchan 的值 = 0xc000090000 intchan本身的地址 = 0xc00008a018 //channel len = 3 cap = 3 //num2 = 10 //channel len = 2 cap = 3 // //goroutine 1 [chan receive]: //main.main() // e:/gostudent/src/2020-04-06/main.go:28 +0x4d4
channel使用的注意事项
-
channel 中只能存放指定的数据类型
-
channel 的数据放满后,就不能再放入了
-
如果从channel取出数据后,可以继续放入
-
在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
读写channel案例演示
- 创建一个intchan,最多可以存放3个int,演示存3个数据到intchan,然后再取出这三个int
func main() { var intchan chan int intchan = make(chan int, 3) intchan <- 10 intchan <- 20 intchan <- 10 //因为intchan 的容量为3,再存放会报告deadlock //intchan <- 50 num1 := <- intchan num2 := <- intchan num3 := <- intchan //因为intchan 这时已经没有数据了,再取会报告deadlock //num4 := <- intchan fmt.printf("num1 = %v num2 = %v num3 = %v", num1, num2, num3) } //num1 = 10 num2 = 20 num3 = 10
- 创建一个mapchan,最多可以存放10个map[string]string的key-val,演示写入和读取
func main() { var mapchan chan map[string]string mapchan = make(chan map[string]string, 2) m1 := make(map[string]string, 2) m1["city1"] = "北京" m1["city2"] = "天津" m2 := make(map[string]string, 2) m2["hero1"] = "宋江" m2["hero2"] = "林冲" mapchan <- m1 mapchan <- m2 num1 := <- mapchan num2 := <- mapchan fmt.printf("num1 = %v num2 = %v", num1, num2) } //num1 = map[city1:北京 city2:天津] num2 = map[hero1:宋江 hero2:林冲]
- 创建一个catchan,最多可以存放10个cat结构体变量,演示写入和读取的用法
type cat struct{ name string age int } func main() { var catchan chan cat catchan = make(chan cat, 10) cat1 := cat{name: "tom", age: 18,} cat2 := cat{name: "zise", age: 18,} catchan <- cat1 catchan <- cat2 //取出 cat11 := <- catchan cat22 := <- catchan fmt.println(cat11, cat22) } //{tom 18} {zise 18}
- 创建一个catchan2,最多可以存放10个*cat变量,演示写入和读取的用法
type cat struct{ name string age int } func main() { var catchan chan *cat catchan = make(chan *cat, 10) cat1 := cat{name: "tom", age: 18,} cat2 := cat{name: "zise", age: 18,} catchan <- &cat1 catchan <- &cat2 //取出 cat11 := <- catchan cat22 := <- catchan fmt.println(*cat11, *cat22) } //{tom 18} {zise 18}
- 创建一个allchan,最多可以存放10个任意数据类型变量,演示写入和读取的用法
type cat struct { name string age int } func main() { var allchan chan interface{} allchan = make(chan interface{}, 10) cat1 := cat{name: "tom", age: 18} cat2 := cat{name: "zise", age: 18} allchan <- cat1 allchan <- cat2 allchan <- 10 allchan <- "jack" //取出 cat11 := <- allchan cat22 := <- allchan v1 := <- allchan v2 := <- allchan fmt.println(cat11, cat22, v1, v2) } //{tom 18} {zise 18} 10 jack
- 看下面的代码,会输出什么
type cat struct { name string age int } func main() { var allchan chan interface{} allchan = make(chan interface{}, 10) cat1 := cat{name: "tom", age: 18} cat2 := cat{name: "zise", age: 18} allchan <- cat1 allchan <- cat2 allchan <- 10 allchan <- "jack" //取出 //cat11 := <- allchan //fmt.println(cat11.name) // # command-line-arguments //src\go_code\chapter15\exec03\test03.go:23:19: cat11.name undefined (type interface {} is interface with no methods) 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) } //newcat = main.cat newcat = {tom 18} //newcat.name = tom
channel的遍历和关闭
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
func main() { intchan := make(chan int, 3) intchan <- 100 intchan <- 200 close(intchan) //close //这时不能够再写入数到channel //intchan <- 300 fmt.println("oko") //当管道关闭后,读取数据是可以的 n1 := <- intchan fmt.println("n1 = ", n1) } //oko //n1 = 100
channel的遍历
channel支持 for - range 的方式进行遍历,注意两个细节
-
在遍历时,如果channel没有关闭,则会出现deadlock的错误
-
在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
channel遍历和关闭的案例演示
func main() { //遍历管道 intchan2 := make(chan int, 100) for i := 0; i < 100; i++ { intchan2 <- i *2 //放入100个数据到管道 } //遍历管道不能使用普通的for循环 //for i := 0; i < len(intchan2); i++ { // //} //1)在遍历时,如果channel没有关闭,则会出现deadlock的错误 //2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 close(intchan2) for v := range intchan2 { fmt.println("v = ", v) } }
应用案例
应用案例-利于管道实现边写边读
请完成goroutine和channel协同工作的案例,具体要求:
-
开启一个writedata协程,向管道intchan中写入50个整数
-
开启一个readdata协程,从管道intchan中读取writedata写入的数据
-
注意:writedata和readdata操作的是同一个管道
-
主线程需要等待writedata和readdata协程都完成工作才能退出【管道】
思路分析
代码区
import ( "fmt" "time" ) //writedata func writedata(intchan chan int) { for i := 1; i <= 50; i++ { //放入数据 intchan <- i fmt.println("writedata", i) time.sleep(time.second) } close(intchan) //关闭 } //readdata 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 } } }
var ( mymap = make(map[int]int, 10) ) func cal(n int) map[int]int { res := 1 for i := 1; i <= n; i++ { res *= i } mymap[n] = res return mymap } func write(mychan chan map[int]int) { for i := 0; i <= 15; i++ { mychan <- cal(i) fmt.println("writer data:", cal(i)) } close(mychan) } func read(mychan chan map[int]int, exitchan chan bool) { for { v, ok := <-mychan if !ok { break } fmt.println("read data:", v) } exitchan <- true close(exitchan) } func main() { var mychan chan map[int]int mychan = make(chan map[int]int, 20) var exitchan chan bool exitchan = make(chan bool, 1) go write(mychan) go read(mychan, exitchan) for { _, ok := <-exitchan if !ok { break } } }
应用案例 - 阻塞
思考:假设我们注销掉go read(mychan,exitchan)会发生什么呢?
也就是说,只有写入mychan而没有读取mychan,当存入mychan里面的数据达到了mychan的容量,再继续存入就会报deadlock错误。同时,由于exitchan需要写入一个true,而exitchan需要读取完mychan中的数据后才写入一个true,但是现在不能进行读取,也就是说,true不会写入exitchan,就形成了阻塞。假设我们打开go read(mychan,exitchan),我们设置其每隔1秒才读取一条数据,而写入则让其正常运行,也就是说,写入很快,读取很慢,这样会导致deadlock吗?答案是不会,只要有读取,golang会有个机制,不会让mychan存储的值超过mychan的容量。
应用案例-求素数
需求
要求统计 1 - 8000的数字中,哪些是素数?
现在具备了goroutine和channel的知识后,就可以完成了
分析思路
传统的方法:使用一个循环,循环的判断各个数是不是素数
使用并发/并行的方式:将统计素数的任务分配给多个(4个)goroutine去完成,完成任务时间短
画出分析思路
说明:有五个协程,三个管道。其中一个协程用于写入数字到intchan管道中,另外四个用于取出intchan管道中的数字并判断是否是素数,然后将素数写入到primechan管道中,最后如果后面四个协程哪一个工作完了,就写入一个true到exit管道中,最后利用循环判断这四个协程是否都完成任务,并退出
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取出数据,并判断是否为素数,如果是,就放入到primechan func 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, 200000) primechan := make(chan int, 200000) //放入结果 //标识退出的管道 exitchan := make(chan bool, 4) // 4个 //开启一个协程,向intchan放入1 - 200000个数 go putnum(intchan) //开启四个协程,从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线程退出") }
升级
package main import ( "fmt" "time" ) func isprime(n int) bool { for i := 2; i <= n; i++ { if n%i == 0 { return false } } return true } //传统方法耗时 func test() { start := time.now() for i := 1; i < 80000; i++ { isprime(i) } cost := time.since(start) fmt.printf("传统方法消耗时间为:%s", cost) } //向intchan放入1 - 80000个数 func putnum(intchan chan int) { for i:= 1; i <= 80000; i++ { intchan <- i } //关闭intchan close(intchan) } //从intchan取出数据,并判断是否为素数,如果是,就放入到primechan func 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 // } //} isp := isprime(num) if !isp { continue } else { primechan <- num } } fmt.println("有一个primenum协程因为取不到数据,退出") //这里还不能关闭primechan //向exitchan 写入true exitchan <- true } func main() { intchan := make(chan int, 200000) primechan := make(chan int, 200000) //放入结果 //标识退出的管道 exitchan := make(chan bool, 4) // 4个 //记录当前时间 start := time.now() //开启一个协程,向intchan放入1 - 200000个数 go putnum(intchan) //开启四个协程,从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 //计算耗时时间 cost := time.since(start) fmt.printf("使用协程耗费时间:%s\n", cost) close(primechan) }() //遍历primechan,把结果取出 for { _, ok := <- primechan if !ok { break } //将结果输出 //fmt.printf("素数 = %d\n", res) } fmt.println("main线程退出") test() } //有一个primenum协程因为取不到数据,退出 //有一个primenum协程因为取不到数据,退出 //有一个primenum协程因为取不到数据,退出 //有一个primenum协程因为取不到数据,退出 //使用协程耗费时间:876.6558ms //main线程退出 //传统方法消耗时间为:3.3300976s
channel使用细节和注意事项
channel可以声明为只读,或者只写性质
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) }
channel只读和只写的最佳实践案例
//ch chan <- int 这样ch就只能写操作了 func send(ch chan <- int, exitchan chan struct{}) { for i := 0; i < 10; i++ { ch <- i } close(ch) var a struct{} exitchan <- a } //ch <- chan int ,这样ch 就只能读操作了 func recv(ch <- chan int, exitchan chan struct{}) { for { v, ok := <- ch if !ok { break } fmt.println(v) } var a struct{} exitchan <- a } func main() { var ch chan int ch = make(chan int, 10) exitchan := make(chan struct{}, 2) go send(ch, exitchan) go recv(ch, exitchan) var total = 0 for _ = range exitchan { total ++ if total == 2 { break } } fmt.println("结束...") }
使用select可以解决从管道取数据的阻塞问题
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 } } }
goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
说明:如果起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,进行处理,这样即使这个协程发生了问题,但是主线程仍然不受影响,可以继续执行。
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) } } //main() ok= 0 //test() 发生错误 assignment to entry in nil map //hello,world //main() ok= 1 //hello,world //main() ok= 2 //hello,world //main() ok= 3 //hello,world //main() ok= 4 //hello,world //main() ok= 5 //hello,world //main() ok= 6 //hello,world //main() ok= 7 //hello,world //main() ok= 8 //hello,world //main() ok= 9 //hello,world
管道的练习题
说明:
-
创建一个person结构体[name,age,address]
-
使用rand方法配合随机创建10个person实例,并放入到channel中
-
遍历channel,将各个person实例的信息显示在终端...
上一篇: [python库]turtle库总结
推荐阅读
-
go语言系列-从数组到map
-
go语言系列-从Goroutine到Channel
-
go语言之行--golang核武器goroutine调度原理、channel详解
-
2018Go语言从入门到精通全套视频
-
详解Go语言的context包从放弃到入门
-
Go语言从入门到规范-6.9、Go处理yml和ini文件
-
c语言从入门到放弃 php从入门到放弃系列-01php环境的搭建
-
区块链开发零基础必备技能之GO语言从入门到高级(go基础、高级特性、区块链概念、应用场景) go语言
-
c语言从入门到放弃 php从入门到放弃系列-02php小Demo
-
为什么go语言能在中国这么火?很多公司的各个业务线都在转go语言,从php到go,从C++到go。