Go语言进阶 | 青训营笔记

152 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

Go语言进阶

并发与并行

并发:多线程程序再一个核的cpu上运行

并行:多线程程序再多个核的cpu上运行

协程与线程

协程:用户态,轻量级线程,栈MB级别

线程:内核态,线程跑多个协程,栈KB级别

主死从随:

  • 主线程退出,协程即使没有执行完也会退出
  • 协程可以在主线程没有退出先自己结束
func main() {
    //启动一个协程
    //使用匿名函数调用匿名函数
    go func() {
        fmt.Println("1")
    }()
    time.Sleep(time.Second * 2) //主线程睡两s
}

image-20230116152601017.png

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    HelloGoRoutine()
}
​
func hello(i int) {
    println("hello goroutine : " + fmt.Sprint(i))
}
​
func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

启动多个协程

func main() {
    for i := 0; i < 5; i++ {
        //启动多个协程
        //使用匿名函数调用匿名函数
        go func(n int) {
            fmt.Println(n)
        }(i) //闭包 i传进来就不会出现5
    }
​
    time.Sleep(time.Second * 2) //主线程睡两s
}

sync

WaitGroup包:控制主线程和协程同时结束

  • wg.Add()
  • wg.Done()
  • wg.Wait()
var wg sync.WaitGroup //只定义 无需赋值
func main() {
    for i := 1; i <= 5; i++ {
        wg.Add(1) //协程开始加1
        go func(n int) {
            defer wg.Done() //协程结束减1
            fmt.Println(n)
        }(i)
    }
    //主线程阻塞 wg计数器减为0结束
    wg.Wait()
}

锁Lock

多个协程控制同一个数据:互斥锁sync.Mutex

确保:一个协程在执行逻辑的时候另一个协程不执行

var v int
var wg sync.WaitGroup
var lock sync.Mutex
​
func main() {
    wg.Add(2)
    go add()
    go sub()
    wg.Wait()
    fmt.Println(v)
}
​
func add() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        //加锁
        lock.Lock()
        v = v + 1
        //解锁
        lock.Unlock()
    }
}
func sub() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
        lock.Lock()
        v = v - 1
        lock.Unlock()
    }
}

读写锁

RWMutex:读的次数比写的次数多的情况

var wg sync.WaitGroup
var lock sync.RWMutex //读写锁
​
func main() {
    wg.Add(6)
    //读多写少
    for i := 0; i < 5; i++ {
        go read()
    }
    go write()
    wg.Wait()
}
​
func read() {
    defer wg.Done()
    lock.RLock()
    fmt.Println("读取数据")
    time.Sleep(time.Second)
    fmt.Println("读取成功")
    lock.RUnlock()
}
func write() {
    defer wg.Done()
    lock.Lock()
    fmt.Println("修改数据")
    time.Sleep(time.Second * 2)
    fmt.Println("修改成功")
    lock.Unlock()
}

channel管道

特点

  • 本质是队列:先进先出
  • 自身线程安全,不需要加锁
  • 有类型:string管道只能存放string类型数据

image-20230116140809919.png

image-20230116140704004.png

引用类型

func main() {
    //定义管道
    var intChan chan int
    //通过make初始化 :存放3个int数据类型的数据
    intChan = make(chan int, 3)
​
    //证明管道是引用类型
    fmt.Printf("intChan值:%v", intChan)
​
    //管道存放数据,不能存放大于容量的数据
    intChan <- 10
    num := 20
    intChan <- num
    //输出管道长度
    fmt.Printf("管道实际长度:%v\n,管道容量:%v\n", len(intChan), cap(intChan))
​
    //从管道中读取数据
    num1 := <-intChan
    fmt.Println(num1) //10
    num2 := <-intChan
    fmt.Println(num2) //20
}

通过make关键字来实现

make(chan 元素类型,[缓冲大小])
  • 无缓冲通道 make(chan int)
  • 有缓冲通道make(chan int,2)

管道的关闭

close(intChan)
    intChan <- 30 //关闭管道不能写
    kk := <-intChan
    fmt.Println(kk) //可以读

管道遍历

通过for-range遍历

func main() {
    //定义管道
    var intChan chan int
    //通过make初始化 :存放3个int数据类型的数据
    intChan = make(chan int, 100)
​
    for i := 0; i < 100; i++ {
        intChan <- i
    }
    close(intChan) //不关闭就会出现死锁的问题
    for v := range intChan {
        fmt.Println(v)
    }
}

实例

A子协程发送0-9数字,B子协程计算输入数字的平方,主协程输出最后的平方数

func main() {
    wg.Add(2)
    src := make(chan int)
    dest := make(chan int, 3)
    go func() { //开启A协程写
        defer close(src) //延迟资源关闭
        wg.Done()
        for i := 0; i < 10; i++ {
            src <- i //存放数据
            fmt.Println("写入的数据:", i)
            time.Sleep(time.Second)
        }
    }()
    go func() { //开启B协程读
        defer close(dest) //关闭管道
        wg.Done()
        for i := range src { //遍历
            dest <- i * i
            fmt.Println("读取的数据:", i)
        }
    }()
    for i := range dest {
        println(i)
    }
    wg.Wait()
}

声明管道只读或只写

var intChan1 chan <- int //只写
var intChan2 <- chan int //只读(空值不读)

管道阻塞

当管道只写入数据,没有读取就会出现阻塞!

select功能

func main() {
    //定义一个int管道
    intChan := make(chan int, 1)
​
    //string
    stringChan := make(chan string, 1)
​
    go func() {
        time.Sleep(time.Second * 3)
        intChan <- 10
    }()
​
    go func() {
        time.Sleep(time.Second * 2)
        stringChan <- "hhhh"
    }()
​
    //利用select
    select {
    case v := <-intChan:
        fmt.Println("intChan:", v)
    case v := <-stringChan:
        fmt.Println("stringChan:", v) //先取stringChan
    default:
        fmt.Println("防止被阻塞")
    }
}

defer+recover机制

防止一个协程出问题导致整个崩掉

func main() {
    go printNum()
    go devide()
    time.Sleep(time.Second * 2)
}
​
func printNum() {
    for i := 1; i <= 10; i++ {
        fmt.Println(i)
    }
}
​
func devide() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("devide错误", err)
        }
    }()
    num1 := 10
    num2 := 0
    result := num1 / num2
    fmt.Println(result)
}

补充知识

defer语义:推迟、延迟

当有多个defer语句时,函数执行到最后defer语句会逆序执行

调用很多defer,那么defer是采用后进先出(栈)模式

defer调用函数的时候,值先传递,最后执行方法

package main
import "fmt"func main() {
    f("1")
    fmt.Println("2")
    defer f("3")
    fmt.Println("4")
    defer f("5")
    fmt.Println("6")
}
​
func f(s string) {
    fmt.Println(s)
}

函数的数据类型

// func() 本身是一个数据类型
// f1不加括号,函数就是一个变量
// 加括号,就是函数的调用
func main() {
​
    fmt.Printf("%T\n", f1)
    //fmt.Printf("%T\n", 10)
    //定义函数类型的变量
    var f3 func(int, int)
    f3 = f1 //执行f3和f1是一样的
    fmt.Println(f3) //地址一样
    fmt.Println(f1) //地址一样
    f3(1, 2)
}
​
func f1(a, b int) {
    fmt.Println(a, b)
}

匿名函数

func main() {
    f1()
    f2 := f1
    f2()
​
    //匿名函数
    f3 := func() {
        fmt.Println("f3")
    }
    f3()
    //函数调用函数本身
    func(a, b int) {
        fmt.Println("f4")
    }(1, 2)
​
    r1 := func(a, b int) int { //func(变量名 变量类型) 返回类型
        return a + b
        //fmt.Println("f4")
    }(1, 2)
    fmt.Println(r1)
}
func f1() {
    println("f1")
}

Go语言支持函数式编程:

  • 将匿名函数作为另一个函数的参数,回调函数
  • 将匿名函数作为另一个函数的返回值,形成闭包结构

回调函数

高阶函数:根据go语言的数据类型的特点,可以将一个函数作为另一个函数的参数

fun1() fun2()

将fun1()函数作为fun2这个函数的参数

fun2函数:成为高阶函数,接收一个函数作为参数的函数

fun1函数:回调函数,作为另外一个函数的参数

package main
​
import (
    "fmt"
)
​
func main() {
    //调用函数
    r1 := add(1, 2)
    fmt.Println(r1)
​
    //高阶函数,add作为参数传递给oper函数
    r2 := oper(10, 20, add) //加
    fmt.Println(r2) //30
​
    r3 := oper(10, 20, sub) //减
    fmt.Println(r3) //30
​
    r4 := oper(5, 5, func(a int, b int) int { //乘
        if b == 0 {
            fmt.Println("不能为0")
        }
        return a / b
    })
    fmt.Println(r4)
}
func add(a, b int) int {
    return a + b
}
func oper(a, b int, fun func(int, int) int) int {
    r := fun(a, b)
    return r
}
​
func sub(a, b int) int {
    return a - b
}
​

闭包结构

一个外层函数,有内层函数,内存函数中会操作外层函数的局部变量

并且外层函数的返回值就是这个内存函数

这个内存函数和外层函数的局部变量统称为闭包结构

package main
​
import "fmt"func main() {
    r1 := increment()
    fmt.Println(r1)
    v1 := r1()
    fmt.Println(v1) //1
    v2 := r1()
    fmt.Println(v2)   //2
    fmt.Println(r1()) //3
    fmt.Println(r1()) //4//
    r2 := increment()
    v3 := r2()
    fmt.Println(v3)   //1
    fmt.Println(r2()) //2
    fmt.Println(r2()) //3
}
​
// 自增
func increment() func() int {
    i := 0 //外层函数局部变量
    //定义一个匿名函数,给变量自增并返回
    fun := func() int { //内层函数,没有执行
        i++
        return i
    }
    return fun
}
​

依赖管理

三要素:

  • 配置文件描述依赖:go.mod
  • 中心仓库管理依赖库:Proxy
  • 本地工具:go get/mod

Go工程实践

测试

  • 回归测试
  • 集成测试
  • 单元测试

单元测试规则

  • 文件以_test.go结尾
  • func TestXxx(*testing.T):命名规范
  • 初始化逻辑放到TestMain中:数据装载、配置初始化等

开源地址测试期望与测试是否一致:开源地址

"github.com/stretchr/testify/assert"

通过命名--cover来查看代码覆盖率(越高越好)

要求:

  • 一般覆盖率:50%-60%
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小、函数单一职责

基准测试:

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供基准测试的能力