这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
本文代码均出自第五届青训营
并发编程
协程Goroutine
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
Go语言可以高效的实现并发,充分利用计算资源,因此速度较快。
协程:用户态,轻量级线程
线程:内核态,线程跑多个协程
Go语言可以通过创建大量的协程来实现高并发。
示例
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func ManyGo() {
for i := 0; i < 5; i++ {
//创建协程
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second) //使主协程在子协程结束前不退出
}
通道Channel
go提倡通信共享内存而不是通过共享内存实现通信,因此需要创建通道来关联多个协程 通过make汉室来创建通道
make(chan int) //无缓冲通道(同步通道)
make(chan int,2) //有缓冲通道,int为元素类型,2为缓冲大小
示例
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
//子协程1
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
//子协程2
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
//主协程
for i := range dest {
println(i) //最后输出0到9的平方
}
}
锁Lock
虽然go提倡通过实现通信来共享内存,但仍然保留了共享内存来实现通信的方式。但在这种方式下,如果操作不当,会产生问题,而通过加锁来解决。
示例
//通过5个协程并发同时执行对变量执行两千次+1操作
var (
x int64
lock sync.Mutex //互斥锁
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock() //在每次+1前获取临界区域的资源
x += 1
lock.Unlock() //+1后将临界区域的权限释放
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x) //10000
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:", x) //7695可能会和我的运行结果不太一样,有随机性
}
不加锁会产生并发安全问题。
线程同步WaitGroup
在前面的代码中,都是用了time.Sleep()函数来结束子协程,但是我们无法知道协程结束的确切时间,实现方式并不优雅,而且在复杂的代码中可能产生问题。使用WaitGroup可以来解决这一问题。
示例
//以下代码将以WaitGroup的方式改写最早的协程示例,来优雅的实现协程
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func ManyGoWait() {
var wg sync.WaitGroup //其实就是内部有一个计数器
wq.Add(5) //计数器+5
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done() //计数器-1
hello(j)
}(i)
}
wg.Wait() //阻塞到计数器为0,计数器为0结束子协程
}
依赖管理
在开发的实际过程中,我们并不是从0开始的,而是通过一些复杂的包来实现。 GO的包管理经历了从GOPATH到Go Vendor再到现在的Go Module的变化
GoPATH
GO语言的环境变量,目录下共有是那个文件夹:
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速赖译
- src:项目源码,项目代码直接依赖src下的代码,通过
go get命令下载的包也存放在此目录下
但是这样的方式存在着弊端:
如果一个包的版本更新没有做到兼容,本地的两个项目分别依赖不同版本的包,无法实现版本的控制。
Go Vendor
在项目目录下增加vendor文件,会先在vendor目录下寻找依赖,再在GOPATH目录下寻找依赖
但这种方式仍不完美,若项目A依赖包B与包C,而包B依赖包D的v1版本,包C依赖包D的v2版本,还是会产生问题。
Go Module
- 通过go.mod配置文件管理依赖版本
- 通过Proxy中心仓库来管理依赖库
- 通过go get/go mod指令管理依赖包
go.mod
module example/project/app //依赖管理基本单元
go 1.16 //依赖的go原生库版本
require ( //单元依赖
example/lib1 v1.0.2
example/lib2 v1.0.0 //indirect 后面加indirect表示间接引用
example/lib3 v1.0.3+incompatible //对于没有go.mod文件包会加上+incompatible后缀,表示可能会产生一些问题
)
Go对版本的命名也做了一定的规范,有两种:
- 语义化版本: Vx.x.x
- 基于commmit伪版本: vx.x.x-yyyymmddhhmmss-abcdefg1234(12位码)
GOPROXY
在依赖分发时,会从存放的站点下载包。
如:GOPROXY="proxy1.cn,https://proxy2.cn,…"
会先从proxy1站点下载包,再从proxy2,最后再从原站下载
go get
通过go get example来安装包,同时也可以增加以下后缀
- @update:最新版本(默认)
- @none:删除依赖
- @vx.x.x:语义版本
- @23dfdd5:特定的commit
- @master:分支的最新commit
go mod
- go mod init:初始化,创建go.mod文件
- go mod download:下载模块到本地缓存
- go mod tidy:增加需要的依赖,删除不需要的依赖
测试
单元测试
命名规范
- 所有测试文件为源文件名+_test
- 测试函数为func TestXxx(t *testing.T)
- 初始化逻辑放到TestMain中
测试操作
通过go test [pkg.go] [pkg_test.go]或是直接使用IDE中的运行来进行测试(将[pkg.go]与[pkg.test.go]的顺序交换好像也可以)
在测试命令最后加上 --cover,可以得出测试代码的覆盖率
有许多外置的包提供了一些测试的函数,如assert包。
在实际项目中一般测试覆盖率为50%到60%,较高的覆盖率为80%以上
测试单元应较小,函数单一职责
Mock测试
通过第三方库进行,如monkey
基准测试
命名规范
- 所有测试文件为源文件名+_test,
- 测试函数为func BenchmarkXxx(b *testing.B)
- 初始化逻辑放到TestMain中
项目实战
项目开发的流程:
- 项目设计
- 代码卡发
- 项目测试
分层结构
- 数据层Repository:数据Model,外部数据的增删改查
- 逻辑层Service:业务Entity,处理核心业务逻辑输出
- 视图层Controller:视图view,处理和外部的交互逻辑