06-信道、互斥锁、异常处理、Gin框架beego的使用

发布时间 2023-04-04 18:54:59作者: Edmond辉仔

1 goroutine 协程

//1 并发和并行
  并发:同一时间段内,多个任务在执行(单个cpu,执行多个任务)
  并行:同一时刻,多个任务在执行(多个cpu的支持)


//注:
  编程语言中,因为Python有GIL全局解释器锁,导致同一时刻,同一个进程中只能运行一个线程 
    ===> 延伸出开启多进程,解决利用上多核优势

  其他语言并发,不存在开启多进程一说,直接开启多线程,就会利用上多核优势

  go并发,没有开启多进程、多线程一说,直接开启go协程 goroutine  //实质是协程 + 线程池


//2 go协程 goruotine 的开启方法
  任务函数调用前 加关键字 go
    eg:  go test()



//3 大小
  协程(goroutine) : 2kb大小
  线程            : 几兆(M)

  //go协程会复用线程,就是GMP模型中,从线程池的一堆线程M中,通过Processor调度器P,去挂载执行go的协程G



//4 协程通信
  goroutine之间通信,通过 信道channel 通信
  go推崇用信道通信,而不推崇用共享变量通信(锁,死锁)


//5 go语言的GMP模型
    -G: 开的goroutine
    -M: M当成操作系统真正的线程,实际上是用户线程,再对应到内核线程(操作系统线程)
    -P: Processor:goroutine的调度器,是一个队列
        gomaxprocs默认是cpu核数(可以当做cpu核数)

  //用户线程 与 内核线程 的映射关系
    python : 开的线程是用户线程,用户线程跟内核线程 是1:1的对应关系
    某些语言: 用户线程和内核线程 是n:1的关系
    go     : 用户线程和内核线程 是n:m的关系  //m < n,一般把m设置为CPU核数



package main

import (
    "fmt"
    "runtime"
    "time"
)


func test()  {
    fmt.Println("go go go")
}


func main() {
    fmt.Println("主线程开始执行")
    go test()    //方法调用前 加关键字 go  --> 开启一个goroutine
    go test()
    go test()
    go test()
    go test()
    go test()
    go test()
    go test()
    time.Sleep(1*time.Second)
    fmt.Println("主线程结束执行")
    //go语言中,主线程不会等待goroutine执行完成,要等待它结束需要自己额外处理
}


func main() {
    fmt.Println("主线程开始执行")
    for i:=0;i<10;i++ {
        go func(){
            fmt.Println("go go go ")
        }()
    }
    time.Sleep(1*time.Second)
    fmt.Println("主线程结束执行")
}



//eg: GMP模型
func main() {
    // 设置P的大小,认为是cpu核数即可
    runtime.GOMAXPROCS(1)
    fmt.Println("主线程开始执行")

    go func() {
        for {
            fmt.Println("xxxx")
        }
    }()

    for i := 0; i < 10; i++ {
        go func() {
            for {
                fmt.Println("我是死循环")
            }
        }()
    }

    time.Sleep(10 * time.Second)
    fmt.Println("主线程结束执行")
}

2 信道(通道)

2.1 基本信道

//不同goroutine之间通信,通过信道channel实现
  
    信道 其实在内存中也是一个共享变量,只不过是处理了线程安全(就是自动有加锁的处理)


package main

import (
    "fmt"
    "time"
)


func main() {
    //1 定义channel
    var c chan int  //定义了一个int类型的信道,协程往里存取数字
    
    //2 信道的零值
        引用类型,空值为nil
        当做参数传递时,不需要取地址,改的就是原来的
        需要初始化再使用
    
    fmt.Println(c)
    
    
    //3 信道初始化
    c=make(chan int)  //数字暂时先不关注
    
    
    //4 信道放值  (注意赋值和放值)
    c <- 1
    
    c = 12  //赋值直接报错
    
    
    //5 信道取值
    <- c
    
    
    //6 取出来赋值给一个变量 int
    var a int
    a = <- c
    
    a := <- c  //推导类型: 定义变量a

    
    //7 信道默认情况下,不管放值还是取值,都是阻塞的
        必须是至少有两个协程 同时对接存取值时,放取值的协程 才不阻塞,继续向下执行
          
        若只是有单个放值 或 取值的协程,会报错死锁: deadlock

    go test1(c)  //开启了一个协程,执行test1函数,向信道c放值 10

    b := <- c    //阻塞  不但实现了两条协程之间通信,还实现了等待协程执行结束
    fmt.Println(b)
}


func test1(a chan int)  {
    time.Sleep(1*time.Second)
    fmt.Println("go go go ")
    
    //往信道中放一个值
    a <- 10 //阻塞
}

2.2 信道小案例

//信道小例子
  程序有一个数中 每一位的平方和与立方和,然后把平方和与立方和相加并打印出来


package main

import (
    "fmt"
    "time"
)

func calcSquares(number int, squareop chan int) {
    sum := 0 //总和
    for number != 0 {
        digit := number % 10 //589对10取余数,9   8   5
        sum += digit * digit //sum=9*9   8*8     5*5
        number /= 10         //num=58    5       0
    }
    time.Sleep(2 * time.Second)
    squareop <- sum
}


func calcCubes(number int, cubeop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    time.Sleep(1 * time.Second)
    cubeop <- sum
}


func main() {
    ctime := time.Now().Unix()
    fmt.Println(ctime)
    
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)

    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    
    squares, cubes := <-sqrch, <-cubech
    //squares:= <-sqrch
    //cubes:=<-cubech
    
    fmt.Println("Final output", squares+cubes)
    
    ttime := time.Now().Unix()
    fmt.Println(ttime)
    fmt.Println(ttime - ctime)
}

2.3 信道死锁、单向信道、关闭信道

//1 信道的死锁现象  deadlock
  默认在多个goroutine中,放值、取值都是阻塞的
     在一个goroutine中,出现若只是有单个放值 或 取值的,就会出现死锁
  

package main

import "fmt"


//信道死锁eg:
func main() {
    var c chan int = make(chan int)

    c<-1  //其实放不进去,阻塞在这,就死锁了
    
    <-c   //没有,取不到,阻塞在这,就死锁了
}



//2 单向信道:只写 或 只读   chan<-   了解
func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    //sendch := make(chan<- int)  //定义了一个只写信道
    sendch := make(chan int)      //定义了一个可读可写信道
    
    go sendData(sendch)           //传到函数中转成只写信道,在goroutine中,只负责写,不能往外读,主协程读
    fmt.Println(<-sendch)         //只写信道一旦读,就有问题
}



///3 关闭信道
func main() {
    sendch := make(chan int)
    //关闭信道
    close(sendch)

    //信道可以用for循环遍历
}



//for循环 遍历信道,如果不关闭会报死锁,如果关闭了,放不进去,循环结束
func producer(chnl chan int) {
    for i := 0; i < 100; i++ {
        fmt.Println("放入了",i)
        chnl <- i
    }
    close(chnl)
}


func main() {
    ch := make(chan int)
    go producer(ch)
    for v := range ch {     // for range 遍历循环
        fmt.Println("Received ",v)
    }
}

2.4 缓冲信道、信道其他

//1 缓冲信道    就是可以指定 信道的长度
  只在缓冲已满的情况,才会阻塞向缓冲信道,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据



package main

import (
    "fmt"
    "sync"
    "time"
)


func main() {
    var c chan int =make(chan int,6)  //无缓冲信道数字是0
    c<-1
    c<-2
    c<-3
    c<-5
    c<-5
    c<-6
    c<-7  //死锁
    
    <-c
    <-c
    <-c
    <-c
    <-c
    <-c
    <-c  //取空了,死锁

    
    //2 长度 vs 容量
    fmt.Println(len(c))  //目前放了多少
    fmt.Println(cap(c))  //可以最多放多少
}



//3 WaitGroup  等待所有goroutine执行完成

func process1(i int, wg *sync.WaitGroup)  {
    fmt.Println("started Goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    
    //一旦有一个完成,减一
    wg.Done()
}


func main() {
    var wg sync.WaitGroup  //没有初始化,值类型,当做参数传递,修改原值,需要取地址(指针)
    //fmt.Println(wg)
    
    for i:=0;i<10;i++ {
        wg.Add(1)          //启动一个goroutine,add加1
        go process1(i,&wg)
    }

    wg.Wait()              //一直阻塞在这,直到调用了10个done,计数器减到零
}



//4 使用信道如何实现 等待所有goroutine(协程)执行完成?

2.5 Select(信道选择)

//select语句: 用于在多个 发送/接受 信道操作中进行选择
  select语句会一直阻塞,直到发送/接收操作准备就绪,谁先准备就绪,就先执行谁的case
  如果有多个信道操作 同时准备完毕,select会随机地选取其中之一执行

  该语法与 switch类似,所不同的是,这里的每个 case语句都是信道操作


//应用场景:***
  1.异步发送多个请求,谁先回来,就先处理谁

  2.非阻塞式IO,请求没回来,处理其他事情


package main

import (
    "fmt"
    "time"
)


func server1(ch chan string) {
    time.Sleep(6 * time.Second)
    ch <- "from server1"
}


func server2(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "from server2"

}


func main() {
    output1 := make(chan string)
    output2 := make(chan string)
    //开启两个协程执行server
    go server1(output1)
    go server2(output2)
    
    select {
    case s1 := <- output1:
        fmt.Println(s1,"ddddd")
    case s2 := <- output2:
        fmt.Println(s2,"yyyy")
    }
}


===================================================

func process(ch chan string) {
    time.Sleep(10500 * time.Millisecond)
    ch <- "process successful"
}

//eg: for + select 模拟实现了非阻塞式io,若信道数据没有获取,执行其他的事
func main() {
    ch := make(chan string)
    go process(ch)
    
    for {
        time.Sleep(1000 * time.Millisecond)
        select {
        case v := <-ch:
            fmt.Println("received value: ", v)
            return
        default:
            // 可以干其他事,模拟非阻塞式io
            fmt.Println("no value received")
        }
    }
}



//死锁
func main() {
    ch := make(chan string)
    select {
    case <-ch:
    }
}



//信道操作都准备完毕,会随机选取case执行
func server1(ch chan string) {
    ch <- "from server1"
}

func server2(ch chan string) {
    ch <- "from server2" 
}

func main() {
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    time.Sleep(1 * time.Second)
    
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}

3 mutex 互斥锁

//1 使用锁的场景
  多个goroutine通过 共享内存(共享变量) 实现数据通信

//2 临界区
  当程序并发地运行时,多个[Go 协程]同时修改共享资源的代码。这些修改共享资源的代码称为临界区。

  //如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的


//3 总结
  1.不同goroutine之间传递数据:共享变量、通过信道
  2.如果是修改共享变量,建议加锁
  3.如果是协程之间通信,用信道


package main

import (
    "fmt"
    "sync"
)

==================================================================
//含有竞态条件的程序: 1000个协程,每个协程都是 x += 1 的操作
var x  = 0  
func increment(wg *sync.WaitGroup) {  
    x = x + 1
    wg.Done()
}

func main() {  
    var w sync.WaitGroup
    for i := 0; i < 1000; i++ {
        w.Add(1)        
        go increment(&w)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}



//eg: 通过共享变量 + 互斥锁
var x  = 0  //全局变量,各个goroutine都可以拿到并且操作

func increment(wg *sync.WaitGroup,m *sync.Mutex) {
    m.Lock()
    x = x + 1
    m.Unlock()
    
    wg.Done()
}

func main() {
    var w sync.WaitGroup
    var m sync.Mutex  //定义一个全局锁,锁是个值类型,函数传递需要传地址
    fmt.Println(m)
    
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w,&m)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

==================================================================

//eg: 通过信道来做
var x  = 0

func increment(wg *sync.WaitGroup, ch chan bool) {
    ch <- true  //缓冲信道放满了,就会阻塞  等到信道数据被取出后,才会执行放值
    x = x + 1
    <- ch
    wg.Done()
}

func main() {
    var w sync.WaitGroup
    ch := make(chan bool, 1)  //定义了一个有缓存,容量大小为1的信道
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, ch)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

4 异常处理

//1 异常处理
  defer   : 延迟执行,并且即便程序出现严重错误,也会执行    defer   v.延迟
  panic   : 主动抛出异常   //相当于python的 raise       panic   n.恐慌
  recover : 恢复程序,继续执行                          recover v.恢复

  defer 主要是 含有defer语句的函数,会在当前这个函数将要返回之前,再调用另一个函数(defer 语句后面的函数)



//2 捕获异常--通用方式:

//python
try:
    可能会错误的代码
except Exception as e:
    出错了怎么处理的代码
    print(e)
finally:
    无论是否出错,都会执行


//go  
// 1.先写 defer注册
defer func() {    //该匿名函数永远会执行
    if error:= recover(); error!=nil{
        //except的东西
        fmt.Println(error)
    }
    //相当于finally,无论是否出错,都会执行
    fmt.Println("我永远会执行,不管是否出错")
}()

//2.再写 可能会错误的代码
panic("可能会出错的代码")



package main

import (
    "fmt"
    "os"
)


func main() {
    defer fmt.Println("我最后执行")      //注册一下,并不执行,等main函数执行完了以后,从下往上执行defer定义的东西
    defer fmt.Println("我倒数第二个打印")
    
    fmt.Println("我先执行")
    
    //模拟切片出错了
    var a []int
    fmt.Println(a[10])
    
    //主动抛出异常
    panic("我出错了")
    
    fmt.Println("ccccc")   //这句就不会执行了,但defer注册的语句 最后都会执行

    ==========================================================
    //假设打开一个文件,中间可能要出错,就可以把关闭文件 用defer延迟执行
    f:=open()
    defer f.close()
    //出错了
}


====================================================

func f1(){
    fmt.Println("f1 f1")
}


func f2(){
    defer func() {           //这个匿名函数永远会执行
        //error:=recover()   //恢复程序继续执行
        //fmt.Println(error) //如果没有错误,执行recover会返回nil    
                               如果有错误,执行recover会放错误信息
        
        if error := recover(); error != nil {
            //表示出错了,打印一下错误信息,程序也恢复了,会继续往下执行
            fmt.Println(error)
        }
        //相当于finally
        fmt.Println("我永远会执行,不管是否出错")
    }()
    
    fmt.Println("f2 f2")
    //panic("主动抛出错误")
}


func f3(){
    fmt.Println("f3 f3")
}


func main() {
    //捕获异常,处理异常,让程序继续运行
    f1()
    f2()
    f3()
}



//3 go的错误处理  go语言每个语句都有可能出错,故都可以返回接受 错误的信息
func main() {
    f, err := os.Open("/test.txt")
    
    // 有错误必须处理完,才能向下执行
    if err != nil {
        fmt.Println(err)
        return
    }
    
    fmt.Println(f.Name(), "opened successfully")
}

5 Gin框架beego的使用

# 1 go语言中的web框架
    gin  beego  Iris  Echo 

# 2 python的web框架
    django flask,tornado,sanic,fastapi

# 3 gin:
   小巧精简的框架,类似于flask,使用第三方插件

# 4 beego:
  大而全,类似于django,缓存,session...

    
# 5 beego快速入门:中国人写的,文档全是中文的
	-go env -w GOPROXY=https://goproxy.io,direct   设置代理

	-go get github.com/astaxie/beego
    -go get github.com/beego/bee   等同于djangoadmin创建项目,创建beego项目
    
    -go list  安装项目依赖的模块