Golang并发、Channel以及异常处理

483 阅读3分钟

go并发

Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建
func Printhello() string {
    fmt.Println("hello")
    return "hello"
}
​
func main()  {
    fmt.Println("主goroutine执行开始")
    go Printhello()  // 开启一个新的goroutine
    fmt.Println("主线程执行结束")
}

信道

如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制,信道是引用类型,初始化为nil
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型
var 信道实例 chan 信道类型  // 创建信道
信道实例 := make(chan 信道类型)  // 创建并实例化信道func Printhello(c chan string) {
    fmt.Println("其他goroutine执行开始")
    c<-"hello"  // 往信道中放值
}
​
func main()  {
    fmt.Println("主goroutine执行开始")
​
    // 信道是一种类型,定义信道
    var a chan string=make(chan string)  // chan是信道类型关键字,string表示信道到存string类型,使用make进行初始化
    go Printhello(a)   // 执行其他goroutine
    b:=<-a  // 接收其他goroutine往信道中取值
    fmt.Println(b)  // hello
    fmt.Println("主goroutine执行结束")
}
​
// 将信道中存值为空接口类型可放任意类型数据
var a chan interface{}=make(chan interface{})
​
// 信道的取值和赋值默认都是阻塞的
func main()  {
    var c chan int=make(chan int)
    fmt.Println("主goroutine")
    go Recive(c)
    c<-1  // 由goroutine发送就必须由goroutine接收,否则会报死锁
}
​
func Recive(c chan int){
    d:=<-c
    fmt.Println("信道中取值",d)
}
​
// 缓冲信道和无缓冲信道
// 创建有缓冲信道,允许信道里存储一个或多个数据,这意味着,设置了缓冲区后,发送端和接收端可以处于异步的状态;无缓冲信道的个数为0,在信道里无法存储数据,这意味着,接收端必须先于发送端准备好,以确保你发送完数据后,有人立马接收数据,否则发送端就会造成阻塞,原因很简单,信道中无法存储数据,也就是说发送端和接收端是同步运行的。
var a chan int= make(chan int, 3)
a<-999
a<-866
fmt.Println(<-a)
go Recive(a)
​
// 长度和容量:长度(信道中有几个值),容量(初始化容量大小)
fmt.Println(len(a))
fmt.Println(cap(a))
​
// 双向信道和单向信道

Waitgroup

// Waitgroup是常用的并发措施,我们可以用它来等待一批goroutine结束。Waitgroup也是一种类型,开了很多的goroutine,主goroutine等待所有goroutine执行完再执行,waitgroup是一个指针类型要传地址
func main() {
    var wg sync.WaitGroup  // 值类型默认零值;如果需要当参数传递并且改变值,需要取地址
    go task("23", &wg)
    go task("王五", &wg)
    go task("张三", &wg)
    wg.Add(3)  // goroutine启动三个任务,需要等待三个任务Done才能执行主任务
    wg.Wait()  // 等待完成所有任务
    fmt.Println("执行主任务结束")
​
}
func task(s string, wg *sync.WaitGroup)  {
    fmt.Println(s)
    wg.Done()  // 表示其中一个任务完成
}
​
func main() {
    var wg sync.WaitGroup  // 值类型默认零值;如果需要当参数传递并且改变值,需要取地址
    //go task("23", &wg)
    //go task("王五", &wg)
    //go task("张三", &wg)
    //wg.Add(3)  // goroutine启动三个任务,需要等待三个任务Done才能执行主任务
    //wg.Wait()  // 等待完成所有任务
    for i:=0;i<5;i++{
        go task1(i, &wg)
        wg.Add(1)
    }
    wg.Wait()
    fmt.Println("执行主任务结束")
    
func task1(s string, wg *sync.WaitGroup)  {
    fmt.Println(s)
    wg.Done()  // 表示其中一个任务完成
}

工作池

/*
缓冲信道的重要应用之一工作池。工作池就是一组等待分配任务的线程,一旦完成所有分配的任务,这些线程就会继续等待任务分配。创建工作池的核心功能:
创建一个go协程,监听一个等待作业的输入型缓冲信道
将作业添加入输入型缓冲信道中
作业完成后,再将结果写入另一个输出型缓冲信道
从输出行缓冲信道读取并打印结果值
*/
// 实现创建一个工作池,计算生成随机数的和
import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)
​
type Job struct {  // 任务
    Id int
    RoundNum int
}
​
type Result struct {  // 结果
    Job Job
    total int
}
​
// 定义两个信道分别存放任务和结果
var jobChan = make(chan Job, 10)
var resultChan = make(chan Result, 10)
​
// 任务随机生成一批数字,把数字放入任务信道中
func genNum(n int)  {
    for i:=0;i<n;i++{
        // 生成随机数
        randNum :=rand.Intn(999)  // 生成小于999的随机数字
        jobChan<-Job{Id: i,RoundNum: randNum}
    }
    // for循环结束任务全部放入信道
    close(jobChan)
}
​
// 执行任务的worker
func worker(wg *sync.WaitGroup)  {
    for job:=range jobChan{  // 循环任务信道取出任务
        num :=job.RoundNum
        total :=0
        for num!=0{
            total += num % 10
            num/=10
        }
        resultChan<-Result{Job: job,total: total}
    }
    wg.Done()
}
​
// 创建工作池也就是决定开多少协程来处理work
func workPool(maxPool int)  {
    var wg sync.WaitGroup
    for i:=0;i<maxPool;i++{
        wg.Add(1)
        go worker(&wg)
    }
    wg.Wait()
    // word处理结束任务结果全在resultJob中
    close(resultChan)
}
​
// 打印结果信道中所有数据
func printResult()  {
    for res:=range resultChan{  // 从结果信道中取值,信道关闭表示任务执行完成
        fmt.Printf("任务id:%d随机数是:%d,的结果是:%d\n",res.Job.Id, res.Job.RoundNum, res.total)
    }
}
​
func main() {
    start :=time.Now()
    // 生成随机数放到任务中
    go genNum(100)
    // 在另一个协程中打印结果
    go printResult()
    // 创建工作池执行任务
    workPool(10)
    end:=time.Now()
    fmt.Printf("运行时间%v\n", end.Sub(start))
}

defer

// defer使用->先注册代码延迟调用,等函数执行结束之后,按照defer注册顺序倒序执行defer,即便程序错误也会执行defer中注册的代码
func mian(){
    a :=10
    defer func(a int) {
        fmt.Println("defer执行匿名函数")
        fmt.Println(a)  // 10
    }(a)  // 参数已经传入
    
    a++
    fmt.Println("执行结束")
}

异常处理

/*
在go中不支持try..catch这种异常的方式。使用defer、panic、recover。
Go中抛出一个panic异常,然后再defer中通过recocer捕获这个异常正常处理。
*/
// panic:主动抛出异常,相当于python中raise
// recover:恢复程序继续执行,返回崩溃的错误信息,recover只有在defer调用的函数中有效
func test(s []int)  {
    defer func() {
        // recover()恢复程序,如果有错会返回错误,如果没有会返回nil
        if err:=recover();err!=nil{  // recover()会接收原函数中panic的错误
            fmt.Println(err)
        }
    }()
    fmt.Println(s)
    panic("主动抛出异常")
}
​
func main() {
    s :=[]int{1,3,4}
    test(s)
    fmt.Println("执行主任务")
}
​
// defer+recover异常处理模板
defer func(){
    if err :=recover();err !=nil{  
        // except代码
    }
    // finally代码      
}()
​
// 自定义错误:Go程序中使用errors.New和panic内置函数
fmt.Errorf("创建一个异常")
errors.New("创建一个异常")
​
// 自定义异常
type error interface {
    Error() string
}
​
type myErr struct {
    s string  // 异常信息
}
​
// 实现error接口
func (e *myErr)Error() string {
    return fmt.Sprintf("错误是%s",e.s)
}
​
​
func getCircleArea(r float32) (float32,error) {
    if r<=0{
        return 0,myErr{s:"半径不能小于0"}
    }
    return 3.14 * r * r,nil
}