协程 | 青训营笔记

85 阅读4分钟

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

协程

并发 多线程程序在一个CPU核心上

并行 多线程程序在多个CPU核心上运行

tip:并行可以是实现并发的一个手段,还有无其他实现手段

Go为何如此之快:实现并发性能极高的并发模型,通过高效调度,充分发挥系统性能

  1. Go可以跑上万个协程

协程是什么

进程:CPU资源分配的最小单位,分为内核态和用户态

线程:CPU调度的最小单位,内核态,栈内存是MB级别,一个线程可以并发跑多个协程

协程:轻量级线程,用户态,栈内存KB级别

补充:

  • 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的
  • 处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。

协程代码实现

关键字:go

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

协程通信

通过通信共享内存,而不是通过共享内存实现通信

Channel

关键字:make

无缓冲通道(同步通道):发送的协程和接收的协程同步化

有缓冲通道:发送的协程和接收的协程同步化,是一个生产者-消费者模型,可以缓解生产者消费者速度不匹配

代码实现

 package main
 ​
 import "time"
 ​
 // 有缓冲的
 func main() {
     channel := make(chan int, 3)
     go func() {
         defer close(channel)
         for i := 0; i < 52; i++ {
             channel <- i
         }
     }()
     go func() {
         defer close(channel)
         for i := range channel {
             println(i)
         }
     }()
     time.Sleep(1 * time.Second)
 }
 package main
 ​
 import "time"
 ​
 // 无缓冲的
 func main() {
     channel := make(chan int)
     go func() {
         for i := 0; i < 30; i++ {
             channel <- i
         }
         close(channel)
     }()
     go func() {
         for i := range channel {
             println(i)
         }
     }()
     time.Sleep(time.Second)
 }

共享内存

关键字:

lock

unlock

 package main
 ​
 // 无锁
 func main() {
     //var lock sync.Mutex
     var ans = 0
     for i := 0; i < 1000; i++ {
         go func() {
             ans++
         }()
     }
     time.Sleep(time.Second)
     println(ans)
 }
 //结果975
 package main
 ​
 import (
     "sync"
     "time"
 )
 ​
 func main() {
     var lock sync.Mutex
     var ans = 0
     for i := 0; i < 5; i++ {
         go func() {
             for i := 0; i < 2000; i++ {
                 lock.Lock()
                 ans += 1
                 lock.Unlock()
             }
         }()
 ​
     }
     // 这句话要加,不然会导致执行到打印的时候协程还没计算完
     time.Sleep(time.Second)
     println(ans)
 }
 //结果10000

WaitGroup

add(n int) 计数器+n

Done() 计数器-1

wait() 阻塞直到计数器为0

 // 使用WaitGroup对最初的代码进行优化
 package main
 ​
 import (
    "fmt"
    "sync"
 )
 ​
 func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
       go func(j int) {
          defer wg.Done()
          fmt.Print("hello" + fmt.Sprintln(j))
       }(i)
 ​
    }
    wg.Wait()
 }
 //使用waitGroup优化上面那段代码
 package main
 ​
 import (
     "sync"
 )
 ​
 func main() {
     var lock sync.Mutex
     var ans = 0
     var wg sync.WaitGroup
     for i := 0; i < 100; i++ {
         wg.Add(1)
         go func() {
             defer wg.Done()
             for i := 0; i < 2000; i++ {
                 lock.Lock()
                 ans += 1
                 lock.Unlock()
             }
         }()
 ​
     }
     wg.Wait()
     println(ans)
 }

依赖管理

经历阶段

GOPATH -> Go Vendor -> Go Module

更迭原因:控制依赖库版本

GOPATH

特点:项目直接应用go/src目录下的依赖

弊端:无法实现不同项目依赖版本不同

GO Vendor

特点:项目目录下增加vendor文件夹,所有依赖首先寻找$projec/vendor下的依赖,如果未发现,则去寻找go/src下的依赖,解决了不同项目依赖版本不同

缺点:无法处理一个项目需要多个不同版本依赖的问题

GoModule

通过go.mod文件管理依赖包版本,通过go get/go mod指令管理依赖包

依赖管理三要素

三要素Go ModuleJava Maven
配置文件,描述依赖go.modpom.xml
中心仓库,管理依赖库Proxy本地仓库/远程仓库
本地控制工具go get/modmvn

依赖配置

配置go.mod

 module awesomeProject1 // 依赖对应的单元
 ​
 // go环境版本
 go 1.19
 ​
 require ( // 单元依赖
     // 间接依赖,后面标识//indirect
     example/lib1 v1.0.2 // indirect
     // 直接依赖
     example/lib2 v1.1.3
     // incompatible示例
     example/lib3 v3.1.2+incompatible
 )

依赖控制

语义化版本

MAJOR.{MAJOR}.{MINOR}.${PATCH}

major:major版本可以版本隔离,一般用于大型更新

minor:要对major版本兼容,通常是新增函数或者小功能更新

patch:主要是BUG修复

例如:v1.2.0

基于commit伪版本

前缀.{前缀}.{时间戳}.${commit哈希码前缀}

例如:v0.0.1-20201130134442-10cb98267c6c

关键字

  • indirect 标识间接接依赖
  • incompatible:处理兼容,因为go要求主版本在2之上的要在模块路径增加/vN后缀,老版本有些没有遵循次规则, 用+incompatible做兼容
 module awesomeProject1 // 依赖对应的单元
 ​
 // go环境版本
 go 1.19
 ​
 require ( // 单元依赖
     // 间接依赖,后面标识//indirect
     example/lib1 v1.0.2 // indirect
     // 直接依赖
     example/lib2 v1.1.3
     // incompatible示例
     example/lib3 v3.1.2+incompatible
 )

编译时选择满足要求的最低版本

依赖分发-回源

直接在git仓库下载

  1. 无法保证构建稳定性,作者修改/删除/增加了软件版本,可能导致构建不可用
  2. 无法保证依赖可用性,作者修改/删除/增加了软件版本,可能导致依赖不可用
  3. 增加第三方压力

Proxy

稳定、可靠,在项目设计过程中也可以借鉴proxy解决思路

Proxy寻找依赖的路径:proxy1->proxy2>direct

工具

Go get

  • 默认拉去major版本最新提交(go get xxxx update)
  • go get xxx @none 删除依赖
  • go get xxx @v1.1.2 @后面跟tag版本,获取指定版本tag依赖
  • go get xxx @23dfdd5 @后面跟commit版本号,获取指定commit版本
  • go get xxx @master @后面跟分支名,获取该分支最新的commit

Go mod

  • go mod init 初始化,创建go.mod文件
  • go mod download 下载模块到本地缓存
  • go mod tidy 增加需要的依赖,删除不需要的依赖,提交代码之前建议执行一遍

测试

回归测试 -> 集成测试 -> 单元测试

覆盖率逐层加大,成本逐层降低

单元测试

目的:保证指令,提升效率

步骤: 输入->测试单元->输出与期望值进行进行校对

单元测试命名规则

  1. 测试文件命令,所有测试文件以_test.go结尾,方便源代码和测试代码在一起,方便测试

  2. 测试函数命名,func TestXxxx(*testing.T)

  3. 初始化逻辑放到func TestMain(m *testing.M)

    1. 运行前:主要进行配置数据装载,配置初始化等前置工作
    2. 运行测试
    3. 测试后进行资源首位工作

测试举例

 // hello_test.go
 package main
 ​
 import "testing"
 ​
 func HelloTom() string {
     return "jerry"
 }
 func TestHelloTom(t *testing.T) {
     output := HelloTom()
     expectOutPut := "jerry"
     if output != expectOutPut {
         t.Errorf("错误")
     }
 }
 // go test hello_test.go
 //ok      command-line-arguments  0.536s
 ​
 ​

单元测试评估

重要指标:代码覆盖率

使用go test xxx xxx --cover

 // judgement.go
 package main
 ​
 func JudgePassLine(score int16) bool {
     if score >= 60 {
         return true
     }
     return false
 }
 // judgement_test.go
 package main
 ​
 import "testing"
 ​
 func TestJudgePassLineTrue(t *testing.T) {
     isPass := JudgePassLine(70)
     if !isPass {
         t.Errorf("未通过")
     }
 }
 === RUN   TestJudgePassLineTrue
 --- PASS: TestJudgePassLineTrue (0.00s)
 PASS
 
 coverage: 66.7% of statements in ./...
  • 一般覆盖率:主流程50%-60%,较高覆盖率80%+(例如支付流程、资金类交易)
  • 基于分支写单元测试,测试分支相互独立、全面覆盖(不重不漏)
  • 测试单元力度足够小,函数职责单一

单元测试依赖

测试要求:

  • 幂等

  • 稳定

    • 不要去调用数据库、cache等,会造成测试结果不稳定
    • 解决方案:使用mock打桩,例如monkey

基准测试

项目实战

数据层(dao):隐藏上层对数据库的处理,上层无需关心数据库、存储相关内容,只需要关心业务核心逻辑

逻辑层(service):处理核心业务的逻辑输出

视图层(controller):视图view,处理和外部交互逻辑

引用

  1. 稀土掘金内部课 后端入门 - Go 语言原理与实践
  2. [从Java 的角度实践 Go 工程| 青训营笔记]  juejin.cn/post/718919…