这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
1 语言进阶
1.1 并行与并发
并行是多线程程序同一时刻在多个核上运行
并发是在一个时间间隔内多个线程交替运行
1.2 goroutine
goroutine简单实列
package main
import (
"fmt"
"time"
)
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() {
HelloGoRoutine()
}
通过go启动协程
运行结果
1.3 CSP(Communicating Sequential Procedure)
1.4 Channel
make(chan 元素类型,[缓冲大小])
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int,2) channel具体使用
package concurrence
//src 子协程发送0-9数字
//dest 子协程计算数字的平方
//主协程输出
//并发安全的
func CalSquare() {
src := make(chan int) //无缓冲通道
dest := make(chan int, 3) //有3个缓冲通道
go func() { //go开启src子协程
defer close(src) //延迟关闭
for i := 0; i < 10; i++ {
src <- i //将数字送入src通道 生产者
}
}()
go func() { //go开启dest子协程
defer close(dest)
for i := range src {
dest <- i * i //dest接收并计算,消费者 消费者消费慢设计为有缓冲通道
}
}()
for i := range dest { //go开启主协程
println(i)
}
}
1.5并发安全Lock
var lock sync.Mutex 定义锁
在对共享变量操作前先加锁
lock.Lock()
操作完毕解锁lock.Unlock()
1.6 WaitGroup
三个主要方法
Add(delta int) 计数器+delta
Done() 计数器减1
Wait() 阻塞直到计数器为0
开启协程 计数器+1 ; 执行结束 计数器-1;主协程阻塞到计数器为0
使用实例
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello goroutine: " + fmt.Sprint(i))
}
func HelloGoRoutine() {
var wg sync.WaitGroup //定义WaitGroup实例
wg.Add(5)//计数器定为5
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()//计数器减1
hello(j)
}(i)
}
wg.Wait()//阻塞
}
func main() {
HelloGoRoutine()
}
2 依赖管理
2.1 Go依赖管理演进
2.1.1 GOPath
- 设置GOPATH环境变量
2. GOPATH 目录下有三个文件夹
- bin 项目编译的二进制文件
- pkg 项目编译的中间产物,加速编译
- src 项目源码
项目代码直接依赖src下的代码
go get 下载最新版的包到src目录下
- 缺点
A和B依赖于某一package的不同版本
无法实现package的多版本控制
2.1.2 Go Vendor
- 在项目目录下增加vendor文件,所有依赖包副本形式放在¥PeojectRott/vendor目录下
- 依赖寻址方式:vendor =》 GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
3.缺点
- 无法控制依赖的版本
- 更新项目可能出现依赖冲突,导致编译错误
2.1.3 Go Module
- 终极目标:定义版本规则和管理项目依赖关系
- 通过go.mod 文件管理依赖包版本
- 通过go get / go mod 指令工具管理依赖包
2.2 依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具go get / mod
2.2.1 依赖配置-文件go.mod
2.2.2 依赖配置-version
- MAJOR: 大版本
- MINOR: 新增函数
- PATCH: 修复
版本前缀-时间戳-12位哈希码
2.2.3 依赖配置-indirect
直接依赖和间接依赖
2.2.3 依赖配置-incompatible
2.2.4 依赖配置-依赖图
正确答案为B
应该选择兼容的最低版本
2.2.5 依赖分发-回源
go module的依赖分发,解决的是从哪里下载,如何下载的问题
github是常见的代码托管系统平台,go module 中定义的依赖,最终可以定义到多版本代码管理系统中某一项目的特定提交或版本,对于go.mod中定义的依赖,则可以直接从对应仓库下载指定软件依赖,完成依赖分发。
存在问题:
- 无法保证构建确定性:软件作者可以直接在代码平台增删改软件版本,导致下次构建使用另外版本的依赖或者找不到依赖版本
- 无法保证依赖可用性:依赖软件作者可以直接在代码平台删除软件,导致依赖不可用
- 增加了第三方代码托管平台压力
2.2.6 依赖分发-proxy
go proxy 是一个服务站点,他会缓存源站中的软件,缓存的软件版本不会改变,并且在源站软件删除后依然可用,从而实现“immutability”和“available”的依赖分发。使用go proxy之后,构建项目时直接中proxy站点获取依赖。
GO Module通过GOPROXY变量控制使用Go Proxy。GPROXY是一个URL列表如上图所示。下载依赖时优先从proxy1下载依赖,如果proxy1不存在则在proxy2下载,proxy依旧不存在则最后会从源站下载,然后缓存到proxy站点中。
设置GOPROXY
go env -w GOPROXY=https://goproxy.cn,direct
2.2.7 工具-go get
- go get example.org/pkg
- @update 默认
- @none 删除依赖
- @v1.1.2 tag版本,语义版本
- @23dfdd5 特定的commit
- @master分支的最新commit
2.2.8 工具-go mod
go mod
- init 初始化,创建go.mod文件
- download 下载模块到本地缓存
- tidy 增加需要的依赖,删除不需要的依赖
3. 测试
- 单元测试
- mock测试
- 基准测试
3.1 单元测试
3.1.1 单元测试-规则
1.所有测试文件以_test.go结尾
2.函数名定义为TestXxx
3.初试化逻辑放到TestMain中
3.1.2 单元测试-例子
被测试函数
测试函数
测试通过
测试不通过
3.1.3 单元测试-assert
使用testify中的assert
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
测试不通过
测试通过
3.1.4 单元测试-覆盖率
覆盖率是用来衡量代码是否经过了足够的测试,评价项目的评测水准,评估项目是否达到了高水准测试等级
覆盖率测试
//被测试代码
package test
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
//测试代码
package test
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
使用go test 命令查看测试覆盖率
go test judgment_test.go judgment.go --cover
加入新的测试方法,提高覆盖率
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
}
Tips
- 一般覆盖率:50%~60%,较高覆盖率80%
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
3.2 单元测试-依赖
幂等:是指每一次测试运行都应该产生与之前一样的结果
稳定:是指相互隔离,能在任何时间、任何环境,运行测试
3.3 单元测试-Mock
monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock 打桩源码
// Patch replaces a function with another
//target 目标函数
//replacement 替代函数
func Patch(target, replacement interface{}) *PatchGuard {
t := reflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
// Unpatch removes any monkey patches on target
// returns whether target was patched in the first place
func Unpatch(target interface{}) bool {
return unpatchValue(reflect.ValueOf(target))
}
monkey patch的作用域在RunTime,在运行时通过GO的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,将待打桩函数的实现跳转打桩函数
使用例子
//被测试函数
package test
import (
"bufio"
"os"
"strings"
)
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
//mock测试
package test
import (
"bou.ke/monkey"
"github.com/stretchr/testify/assert"
"testing"
)
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
通过patch对ReadFirstline进行打桩mock,默认返回line110,这里通过deferi卸载mock,这样整个测试函数就拜托了本地文件的束缚和依赖。
3.3 基准测试
基准测试是指测试一段程序的运行性能及耗费CPU的程度。在实际开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码进行性能分析,这就用到了基准测试。
标题:「Go 语言上手 - 工程实践」第三届字节跳动青训营 - 后端专场