go 语言入门指南:基本语法和常用特性解析(管道channel)
管道 channel
需求引入:计算1-200的各个数的阶乘,并且把各个阶乘放入到map中,最后显示出来,用goroutine实现
分析思路: 1)使用goroutine来完成,效率高,但是会出现并发/并行安全问题 2)由此提出了不同goroutine如何通信的问题 代码实现:
package main
import (
"fmt"
"time"
)
var (
myMap = make(map[int]int, 10) // 【第二个问题 】fatal error: concurrent map writes
)
func test(n int) {
res := 1
for i := 1; i < n; i++ {
res = res * i
}
//将res放入myMap
myMap[n] = res
}
func main() {
//起了200个协程
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10s 【第一个问题】
time.Sleep(time.Second * 10)
//输出结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
代码运行的结果:
运行go build -race main.go 生成main.exe的执行结果(观察竞争关系):
不同goroutine之间是如何通讯的? 1)全局变量加锁同步 2)channel
使用全局变量加锁同步改进程序 因为没有对全局变量map没有加锁,就会出现资源竞争的问题,代码就会出现错误,fatal error: concurrent map writes 解决方案: 加入互斥锁 说明: 数的阶乘会比较大,计算结果就会越界,可以把求阶乘改为sum+=uint64[] 加入互斥锁后的代码:
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap = make(map[int]int, 10) // 【第二个问题 】fatal error: concurrent map writes
//声明一个全局互斥锁
//lock是一个全局的互斥锁
lock sync.Mutex
)
func test(n int) {
res := 1
for i := 1; i < n; i++ {
res = res * i
}
//将res放入myMap
//加锁
lock.Lock()
myMap[n] = res //fatal error: concurrent map writes
//解锁
lock.Unlock()
}
func main() {
//起了20个协程
for i := 1; i <= 20; i++ {
go test(i)
}
//休眠10s 【第一个问题】
time.Sleep(time.Second * 10)
//输出结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
运行结果:
为什么需要channel? 前面使用全局变量加锁同步来解决goroutine的通讯,但并不完美: 1) 主线程在等待所有的goroutine全部完成的时间很难确定,代码里设置的10s,仅仅为估算 2) 如果主线程的休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作的状态,这个时候协程就会随着主线程的退出而销毁 3) 通过全局变量加锁来实现通讯,也不利于多个协程对全局变量的读写操作 由此,引出了新的通讯机制-------channel
channel的介绍 1) channel本质就是一个数据结构-队列,先进先出[FIFO] 2) 线程安全,多goroutine访问时,不需要加锁,也就是说channel本身是线程安全的 3) channel 是有类型的,一个string的channel只能存放string类型的数据
定义与声明channel
package main
func main() {
//定义与声明 channel
//var 变量名 chan 数据类型
var intChain chan int
var mapChain chan map[int]string
var perChain chan Person
var perChan2 chan *Person
}
说明: channel是引用类型,channel必须初始化后才能使用,即make后才能使用;管道是有类型的,intChain只能写入整数int
管道的初始化: 管道的读、写以及创建
package main
import "fmt"
func main() {
//1.一个存放三个int类型的管道
var intChain chan int
intChain = make(chan int, 3)
//2. intChain是什么?
// intChain 的值=0xc000016180 intChain 本身的地址是=0xc000006028
fmt.Printf("intChain 的值=%v intChain 本身的地址是=%p", intChain, &intChain)
//3 向管道写入数据
intChain <- 10
num := 211
intChain <- num
// 注意:当给管道写入数据的时候。不能超过其容量
//4 看看管道的长度与容量cap
//len=2 cap=3
fmt.Printf("channel len=%v,cap=%v\n", len(intChain), cap(intChain))
// 5 从管道中读取数据
var num2 int
num2 = <-intChain
fmt.Println("num2=", num2)
fmt.Printf("channel len=%v,cap=%v", len(intChain), cap(intChain))
//6 在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报 deadlock
}
channel使用的注意事项
1.channel中只能存放指定的数据类型 2.channel的数据放满后,就不能再放入了。3.从channel中取出数据后,可以继续放入 4.在没有使用协程的情况下,如果channel数据取完了,再取,就会死锁 channel读写操作的接口层面的类型断言演示demo:
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func main() {
//定义一个存放任意数据类型的管道 3个数据
var allchan chan interface{}
allchan = make(chan interface{}, 3)
allchan <- 3
allchan <- "tom and jerry"
cat := Cat{"tom", 3}
allchan <- cat
//获取到管道第三个数据
<-allchan
<-allchan
newCat := <-allchan //管道中取出
fmt.Printf("newCat=%T ,newCat=%v\n", newCat, newCat)
//类型断言
a, _ := newCat.(Cat)
fmt.Printf("newCat.Name=%v", a.Name)
}
channel的遍历与关闭
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel中读取数据
案例演示:
package main
func main() {
intChain := make(chan int, 3)
intChain <- 100
intChain <- 43
close(intChain) //close
intChain <- 1 //panic: send on closed channel
}
channel的遍历
channel支持for-range的方式进行遍历,注意两个细节:
1)在遍历时,如果channel没有关闭,则抛出deadlock的错误
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 演示demo:
package main
import "fmt"
func main() {
intChain := make(chan int, 100)
for i := 0; i < 100; i++ {
intChain <- i * 2
}
//遍历
//for i := 0; i < len(intChain); i++ {
// fmt.Println(<-intChain)
//}
close(intChain)
for v := range intChain {
fmt.Println("v=", v)
}
}
goroutine与channel应用案例:
代码:
package main
import (
"fmt"
)
//write
func writeData(intChan chan int) {
for i := 0; i < 50; i++ {
intChan <- i
fmt.Printf("writeData 写入数据=%v\n", i)
}
close(intChan) //关闭
}
//read
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Printf("readData 读到数据=%v\n", v)
}
//读取完数据后,任务完成
exitChan <- true
close(exitChan)
}
func main() {
//创建俩个管道
intChain := make(chan int, 50)
exitChain := make(chan bool, 1)
go writeData(intChain)
go readData(intChain, exitChain)
//time.Sleep(time.Second)
for {
_, ok := <-exitChain
if !ok {
break
}
}
}
说明:如果编译器发现一个管道只有写,而没有读,则该管道就会阻塞。写管道与读管道的频率不一致,则无所谓。
**利用协程与管道求解素数问题的实现1: **
package main
import (
"fmt"
"time"
)
func putNum(intchain chan int) {
for i := 1; i <= 8000; i++ {
intchain <- i
}
//关闭intchain
close(intchain)
}
func primeNum(intchain chan int, primeChain chan int, exitchan chan bool) {
//使用for循环
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <-intchain
if !ok { //intchain取不到数了
break
}
flag = true
//判断num是不是素数
for i := 2; i < num; i++ {
if num%i == 0 { //不是素数
flag = false
break
}
}
//放入primeChain
if flag {
primeChain <- num
}
}
fmt.Println("有一个primeNum 协程因为取不到数据而退出了")
//现在还不能关闭 primeChain
// 向exitChan 中写入true
exitchan <- true
}
func main() {
intchain := make(chan int, 1000)
primeChain := make(chan int, 2000) //放入结果
exitchan := make(chan bool, 4) //4个协程的标志位
//开启一个协程,向intchain放入1-8000个数
go putNum(intchain)
//开启4个协程,从intchain取出数据。并判断是否为是素数,如果是则放入到 primeChain中
for i := 0; i < 4; i++ {
go primeNum(intchain, primeChain, exitchan)
}
//这里的主线程 需要进行处理
go func() {
for i := 0; i < 4; i++ {
<-exitchan
}
//从exitchan取出4个结果,就可以关闭primeChain
close(primeChain)
}()
//遍历primeChain 把结果取出
for {
res, ok := <-primeChain
if !ok {
break
}
// 将结果进行输出
fmt.Printf("素数=%d\n", res)
}
fmt.Println("主线程退出")
}
**channel的可读可写、只能读以及只能写 **
package main
import "fmt"
func main() {
//管道可以声明为只读或者只写
//1.在默认的情况下,管道是双向的
//var chan1 chan int //可读可写
//2 声明为只写
var chan2 chan<- int
chan2 = make(chan int, 2)
chan2 <- 1
chan2 <- 3
//3 声明为只读
var chan3 <-chan int
num := <-chan3
fmt.Println(num)
}
**使用select 可以解决从管道中取数据的阻塞问题 **
package main
import (
"fmt"
"strconv"
"time"
)
func main() {
//使用select 可以解决从管道中取数据的阻塞问题
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
stringCham := make(chan string, 5)
for i := 0; i < 5; i++ {
stringCham <- strconv.Itoa(i) + "hello"
}
//传统的方法在遍历管道的时候,如果不关闭,则会导致死锁 deadlock
//可以使用select方式进行解决
for {
select {
case v := <-intChan: //注意:这里,如果intChan一直没有关闭,不会一直阻塞而死锁
//会自动的到下一个case 进行匹配
fmt.Printf("从intchan中读取了数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringCham:
fmt.Printf("从stringchan中读取了数据%s\n", v)
time.Sleep(time.Second)
default:
fmt.Println("都取不到了,程序员可以加入逻辑")
time.Sleep(time.Second)
return
}
}
}
**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() {
if err := recover(); err != nil {
fmt.Println("test 发生异常了", err)
}
}()
var myMap map[int]string
myMap[0] = "golang" //error panic: assignment to entry in nil map
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main", i)
time.Sleep(time.Second)
}
}