这是我参与「第五届青训营 」笔记创作活动的第2天
协程
并发 多线程程序在一个CPU核心上
并行 多线程程序在多个CPU核心上运行
tip:并行可以是实现并发的一个手段,还有无其他实现手段
Go为何如此之快:实现并发性能极高的并发模型,通过高效调度,充分发挥系统性能
- 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 Module | Java Maven |
|---|---|---|
| 配置文件,描述依赖 | go.mod | pom.xml |
| 中心仓库,管理依赖库 | Proxy | 本地仓库/远程仓库 |
| 本地控制工具 | go get/mod | mvn |
依赖配置
配置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
)
依赖控制
语义化版本
{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仓库下载
- 无法保证构建稳定性,作者修改/删除/增加了软件版本,可能导致构建不可用
- 无法保证依赖可用性,作者修改/删除/增加了软件版本,可能导致依赖不可用
- 增加第三方压力
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 增加需要的依赖,删除不需要的依赖,提交代码之前建议执行一遍
测试
回归测试 -> 集成测试 -> 单元测试
覆盖率逐层加大,成本逐层降低
单元测试
目的:保证指令,提升效率
步骤: 输入->测试单元->输出与期望值进行进行校对
单元测试命名规则
-
测试文件命令,所有测试文件以
_test.go结尾,方便源代码和测试代码在一起,方便测试 -
测试函数命名,
func TestXxxx(*testing.T) -
初始化逻辑放到
func TestMain(m *testing.M)中- 运行前:主要进行配置数据装载,配置初始化等前置工作
- 运行测试
- 测试后进行资源首位工作
测试举例
// 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,处理和外部交互逻辑
引用
- 稀土掘金内部课 后端入门 - Go 语言原理与实践
- [从Java 的角度实践 Go 工程| 青训营笔记] juejin.cn/post/718919…