go 语言入门指南:基本语法和常用特性解析2|青训营

66 阅读7分钟

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)  
}  
}

代码运行的结果:

image.png 运行go build -race main.go 生成main.exe的执行结果(观察竞争关系):

image.png

不同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)  
    }  
}

运行结果:

image.png

为什么需要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应用案例:

image.png

代码:

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)  
    }  
}