这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
语言进阶 从并发编程的视角了解Go高性能的本质
依赖管理 了解Go语言依赖管理的演进路线
测试 从单元测试实践出发,提升质量意识
项目实战 通过项目需求,需求拆解,逻辑设计,代码实现感受真正的项目开发
01语言进阶 并发编程
01并发vs并行
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
1.1Goroutine
协程:用户态,轻量级线程,栈MB级别
线程:内核态,线程跑多个协程,栈KB级别
快速打印 快速:开多个协程去打印
go语言开启一个协程:在调用函数时,在函数前面加上一个go关键字
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()
}
输出
1.2CSP(Communicating Sequential Processes)
提倡通过通信共享内存而不是通过共享内存而实现通信
通过通信共享内存:通道
通过共享内存实现通信:临界区
1.3Channel
make(chan 元素类型,[缓冲大小])
- 无缓冲通道 make(chan int) (同步通道)
- 有缓冲通道 make(chan int,2)
实现
package main
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
//A 子协程发送0~9数字
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
//B 子协程计算输入数字的平方
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
//主协程输出最后的平方数
for i := range dest {
//复杂操作
println(i)
}
}
func main() {
CalSquare()
}
输出
1.4并发安全Lock
package main
import (
"sync"
"time"
)
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func Add() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
}
func main() {
Add()
}
输出
1.5WaitGroup
三个方法: Add(计数器+delta) Done(计数器-1) Wait(阻塞直到计数器为0)
计数器:开启协程+1;执行结束-1;主协程阻塞直到计数器为0
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
func main() {
ManyGoWait()
}
输出
02依赖管理
2.1Go依赖管理演进
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
2.1.1GOPATH
- 环境变量 $GOPATH bin:项目编译的二进制文件
pkg:项目编译的中间产物,加速编译
src:项目源码
- 项目代码直接依赖src下的代码
- go get下载最新版本的包到src目录下
弊端
场景:A和B依赖于某一package的不同版本
问题:无法实现package的多版本控制
2.1.2Go Vendor
- 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:vendor=>GOPATH(项目依赖会先到verdor下获取,没有再到GOPATH) 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
弊端
问题:
- 无法控制依赖的版本
- 更新项目又可能出现依赖冲突,导致编译出错
2.1.3Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包 终极目标:定义版本规则和管理项目依赖关系
2.2依赖管理三要素
- 配置文件,描述依赖(go.mod)
- 中心仓库管理依赖库(Proxy)
- 本地工具(go get/mod)
2.3依赖配置
2.3.1依赖配置-go.mod
2.3.2依赖配置-version
- 语义化版本
${MAJOR}.${MINOR}.${PATCH}
V1.3.0
V2.3.0
- 基于commit伪版本
vx.0.0-yyyymmddhhmmss-abcdefgh1234
v0.0.0-20220401081311-c38fb59326b7
v1.0.0-20201130134442-10cb98267c6c
2.3.3依赖配置-indirect
2.3.4依赖配置-incompatible
- 主版本2+模块会在模块路径增加/vN后缀。
- 对于没有go.mod文件并且主版本2+的依赖,会+incompatible
依赖图
选择最低的兼容版本
2.3依赖分发
2.3.5依赖分发-回源
2.3.5依赖分发-Proxy
2.3.6依赖分发-变量 GOPROXY
GOPROXY="proxy1.cn, proxy2.cn ,direct"
服务站点URL列表,“direct"表示源站
Proxy1->Proxy2->Direct
2.3工具
2.3.7工具-go get
go get example.org/ pkg
- @update 默认
- @none 删除依赖
- @v1.1.2 tag版本,语义版本
- @23dfdd5 特定的commit
- @master 分支的最新commit
2.3.8工具-go mod
go mod
- init 初始化,创建go.mod文件
- download 下载模块到本地缓存
- tidy 增加需要的依赖,删除不需要的依赖
03测试
3.1单元测试
3.1.1规则
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中
3.1.2例子
package main
import "testing"
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
输出
3.1.3运行
go test[flags][packages]
3.1.4单元测试-assert
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func HelloTom() string {
return "Tom"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
3.1.5单元测试-覆盖率
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
3.1.5单元测试-Tips
- 一般覆盖率: 50%~60%,较高覆盖率80%+。
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责。
3.2单元测试-依赖
3.3单元测试-文件处理
package main
import (
"bufio"
"os"
"strings"
"testing"
)
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
}
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
3.4单元测试-Mock
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩
package main
import (
"reflect"
"testing"
)
func Patch(target, replacement interface{}) *PatchGuard {
t := reflect.ValueOf(target)
t := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
func Unpatch(target interface{}) bool {
return unpatchValue(reflect.ValueOf(target))
}
func TestProcessFirstLineWithMock(t *testing.T) {
monkey . Patch(ReadFirstLine, func() string {
return "Line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert. EquaL(t, "line000", line)
}
对ReadFirstLine打桩测试,不再依赖本地文件
3.5基准测试
测试一段程序运行时的性能和cpu的损耗
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
package main
import (
"math/rand"
"testing"
)
/**
随机选择执行服务器
*/
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
func BenchmarkSelect(b *testing.B) {
InitServerIndex() //初始化服务器列表
b.ResetTimer() //定时器重置 InitServerIndex不属于测试服务器的损耗 去掉
for i := 0; i < b.N; i++ {
Select()
}
}
//并行 性能有劣化
//原因:Select sdk 用到了rand函数 rand函数为了保证全局的随机性和并发安全 持有一把全局锁
func BenchmarkSelectParallel(b *testing.B) { //并行
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
优化
//fastrand 牺牲了一定的随机数列的一致性
func FastSelect() int {
return ServerIndex[fastrand.Intn(10)]
}
04项目实践
4.1需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一一个本地web服务
- 话题和回帖数据用文件存储
4.2需求用例
4.3ER图-Entity Relationship Diagram
4.4分层结构
- 数据层:数据Model,外部数据的增删改查
- 逻辑层:业务Entity,处理核心业务逻辑输出
- 视图层视图view,处理和外部的交互逻辑
4.5组件工具
- Gin高性能go web框架
- Go Mod
4.6Repository
index
查询
4.7Service
实体
流程:参数校验->准备数据->组装实体
4.8Controller
构建view对象
业务错误码
4.9Router
初始化数据索引
初始化引擎配置
构建路由
启动服务