并发编程
并发 是多线程程序在一个核的cpu上运行
并行 是多线程程序在多个核的上运行
Go可以充分发挥多核优势,高效运行
一个重要概念
协程
协程的开销比线程小,可以理解为轻量级的线程,一个Go程序中可以创建上万个协程。
Go 中 开启协程 非常简单,在函数前面增加一个 go 关键字就可以为一个函数开启一个协程。
CSP 与 Channel
CSP(Communicating Sequential Process)
Go 中提倡通过 通信共享内存 而不是通过共享内存而实现通信
那么如何通信呢,通过 channel
Channel
语法: make(chan 元素类型, [缓冲大小])
无缓冲通道 make(chan int)
有缓冲通道 make(chan int, 2)
这个图就非常的生动形象~
以下是一个例子:
第一个协程 作为生产者发送0~9 到 src中
第二个协程 作为消费者计算 src 中每个数的平方发送到 dest 中
主线程输出 dest 中每个数
可以看到每次都会是顺序输出,代表着Go是 并发安全的
Go 语言也保留了共享内存的做法,使用sync进行同步,如下
ps:试了好多次都没冲突,乐。把运算稍微改复杂一点就有冲突了
依赖管理
任何大型项目开发都绕不开依赖管理,Go中的依赖主要经历了 GOPATH -> Go Vendor -> Go Module的演变 而现在主要采用Go Module的方式
不同环境依赖的版本不同,所以如何控制依赖库的版本?
GOPATH
项目代码直接依赖src下的代码
通过 go get 下载最新版本的包到src目录下
这样的话,就会出现一个问题:无法实现多版本的控制(A、B依赖于同一个包的不同版本,寄)
Go Vender
项目目录下新增 vendor文件,所有依赖包副本形式放在其中
通过 vendor => GOPATH 的方式曲线救国
ps:感觉挺像前端的package.json……依赖问题真是绕不过去
这又产生了新的问题:
无法控制依赖的版本
更新项目时可能出现依赖冲突,从而导致编译出错
Go Module
通过 go.mod 文件管理依赖包版本
通过 go get/go mod 指令工具管理依赖包
达成了终极目标:既能定义版本规则,又能管理项目依赖关系
可以类比一下Java中的Maven
依赖配置 go.mod
依赖标识语法:模块路径+版本来进行唯一标识
[Module Path][Version/Pseudo-version]
如上,需要注意的是:
主版本2+的模块会在路径后增加/vN后缀
对于没有go.mod文件且主版本2+的依赖,会 +incompatible
依赖的版本规则分为语义化版本和基于commit的伪版本
语义化版本
格式为:{MINOR}.${PATCH} V1.3.0、V2.3.0、 ……
不同的 MAJOR 版本表示是不兼容的API
即使是同一个库,MAJOR 版本不同也会被认为是不同的模块
MINOR 版本通常是新增函数或功能,向后兼容
而 PATCH 版本一般是 修复 bug
基于commit的版本
格式为:${vx.0.0-yyyymmddhhmmss-abcdefgh1234}
版本前缀是和语义化版本一样的
时间戳 (yyyymmddhhmmss),也就是提交 Commit 的时间
校验码 (abcdefgh1234), 12 位的哈希前缀
每次提交 commit 后 Go 都会默认生成一个伪版本号
小测试
如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,依赖图如上,最终编译时所使用的C项目的版本为如下哪个选项?(单选)
A. v1.3
B. v1.4
C. A用到c时用v1.3编译,B用到c时用v1.4编译
答案为:B 选择最低的兼容版本
依赖分发
这些依赖去哪里下载呢?就是依赖分发
在github等代码托管系统中对应仓库上下载?
github是比较常见给的代码托管系统平台,而Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本
对于 go.mod 中定义的依赖,可以从对应仓库中下载指定软件依赖,从而完成依赖分发。
问题也有:
无法保证构建确定性
软件作者直接修改软件版本,导致下次构建使用其他版本的依赖,或者找不到依赖版本
无法保证依赖可用性
软件作者直接代码平台删除软件,导致依赖不可用
增加第三方代码托管平台压力。
通过Proxy方式来解决以上问题
Go Proxy 是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用
使用 Go Proxy 之后,构建时会直接从 Go Proxy 站点拉取依赖。
Go Modules通过 GOPROXY 环境变量控制如何使用 Go Proxy
服务站点URL列表,direct表示源站:GOPROXY="proxy1.cn, proxy2.cn,direct"
GOPROXY是一个 Go Proxy 站点URL列表,可以使用 direct 表示源站。整体的依赖寻址路径,会优先从 proxy1 下载依赖,如果 proxy1 不存在,就下到 proxy2寻找,如果proxy2 也不存在则会回源到源站直接下载依赖,缓存到 proxy 站点中。
工具
测试
测试一般分为回归测试、集成测试、单元测试,从前到后覆盖率逐层变大,成本却逐层降低,所以单元测试的覆盖率一定程度上决定这代码的质量。
回归测试一般是QA同学手动通过终端回归一些固定的主流程场景
集成测试是对系统功能维度做测试验证
单元测试测试开发阶段,开发者对单独的函数、模块做功能验证
单元测试主要包括:输入、测试单元、输出以及校对
单元的概念较广,包括接口,函数,模块等,用最后的校对来保证代码的功能与我们的预期相符
单元测试有以下几点好处
保证质量
整体覆盖率足够时下,既保证了新功能正确性,又未破坏原有代码的正确性
提升效率
代码有bug的情况下,通过单测,可以在一个较短周期内定位和修复问题
Go中的单元测试有以下规则:
所有测试文件以 _test.go 结尾
func TestXxx(testing.T)
初始化逻辑放到 TestMain函数中(测试前的数据装载配置、测试后的释放资源等)
例子:
main.go
main_test.go
在实际项目中,单测覆盖率
一般项目的要求是50%~60%覆盖率
对于重要的资金型服务,覆盖率可能要求达到80%
单测需要保证稳定性和幂等性
稳定是指相互隔离,能在任何时间,任何环境,运行测试
幂等是指每一次测试运行都应该产生与之前一样的结果
而要实现这一目的就要用到mock机制。
bouk/monkey: Monkey patching in Go
monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock,反射,指针赋值Mockey Patch 的作用域在 Runtime,在运行时通过 Go 的 unsafe 包,能够将内存中函数A的地址替换为运行时函数B的地址,将待打桩函数的实现跳转。
Go 语言还提供了基准测试框架
基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。
而我们在实际项目开发中,经常会遇到代码性能瓶颈问题,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试