这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
Go语言进阶
并发与并行
并发:多线程程序再一个核的cpu上运行
并行:多线程程序再多个核的cpu上运行
协程与线程
协程:用户态,轻量级线程,栈MB级别
线程:内核态,线程跑多个协程,栈KB级别
主死从随:
- 主线程退出,协程即使没有执行完也会退出
- 协程可以在主线程没有退出先自己结束
func main() {
//启动一个协程
//使用匿名函数调用匿名函数
go func() {
fmt.Println("1")
}()
time.Sleep(time.Second * 2) //主线程睡两s
}
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类型数据
引用类型
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%
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小、函数单一职责
基准测试:
- 优化代码,需要对当前代码分析
- 内置的测试框架提供基准测试的能力