3-工程实践
3.1 语言进阶
3.1.1 并发 VS并行
并发:多线程程序在一个核的CPU上运行,实质是时间片的切换
并行:多线程程序在多个核的COU上运行
并行可以被理解为是实现并发的一个手段
GO能够实现高并发
3.1.2 Goroutine
协程:用户态,轻量级线程,栈是KB级别;创建和调用由go语言本身完成
例子:简单开启一个协程,在调用函数前加go关键字
输出为乱序输出,因为是并行的
package main
import (
"fmt"
"time"
)
func main(){
for i:=0;i<5;i++{
go func (j int){
hello(j)
}(i)
}
time.Sleep(time.Second)
}
func hello(i int){
fmt.Println("hello goroutine " , i)
}
附:建立一个项目,进入到项目文件夹后建立.mod文件
参考(53条消息) 使用VS code快速搭建一个Golang项目_vscode创建go项目_酷尔。的博客-CSDN博客
3.1.3 CSP
CSP:communicating sequential processes
协程间通信:通过通信来共享内存 而不是 通过共享内存而实现通信
通道channel:把协程进行连接,相当于一个传输队列,遵循先进先出,保证数据的收发顺序。将数据从一个goroutine传输给另一个
也保留使用共享内存进行通信:通过互斥量,临界区
3.1.4 channel
创建:make(chan 元素类型, [缓冲大小])
无缓冲通道make(chan int) :同步通道
有缓冲通道make(chan int, 2) :解决同步问题,典型的生产消费模型
package main
import (
"fmt"
)
func main() {
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 //把生产的数字发送到src这个channel
}
}()
//B协程接收A,并计算
go func() {
defer close(dest)
for i := range src { //遍历通道只有一个参数i
dest <- i * i
}
}()
//接收B协程
for i := range dest {
fmt.Println(i)
}
}
输出:0 1 4 9 16 25 36 49 64 81
3.1.5 并发安全Lock
通过共享内存来实现通信,存在多个核同时操作一个内存地方的情况
开发过程中,要避免对共享内存的非并发安全的读写操作
例子:5个协程并发执行对同一变量做两千次操作
package main
import (
"sync"
"time"
"fmt"
)
var(
x int64
lock sync.Mutex
)
func addWithLock(){
for i:=0;i<2000;i++{
lock.Lock() //加锁,通过临界区控制
x++
lock.Unlock() //解锁
}
}
func addWithoutLock(){
for i:=0;i<2000;i++{
x++
}
}
func main(){
x = 0
for i:=0;i<5;i++{
go addWithLock()
}
time.Sleep(time.Second)
fmt.Println("withLock:",x)
x = 0
for i:=0;i<5;i++{
go addWithoutLock()
}
time.Sleep(time.Second)
fmt.Println("withoutLock:",x)
}
输出:
withLock: 10000 withoutLock: 8096 (随机)
3.1.6 WaitGroup
前面用Sleep做暴力阻塞,不够优雅
不明确子协程精确地结束时间,因此无法精确地Sleep
WaitGroup包括3个方法:
Add(delta int) 计数器+delta
Done() 计数器-1
Wait() 阻塞直到计数器为0
例子:n个并发协程任务,计数器可以+n,每个任务结束后可以调用Done方法使计数器-1,计数器为0表示所有并发任务都已经完成。
package main
import (
"sync"
"fmt"
)
func main(){
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++{
go func(j int){
defer wg.Done() //每个协程执行完后调用done方法对计数器-1
fmt.Println("hello ",j)
}(i)
}
wg.Wait() //阻塞
}
输出:乱序随机
hello 3 hello 1 hello 2 hello 0 hello 4
3.2 依赖管理
3.2.1 背景
依赖指各种开发包,他人已经开发好的包或经过验证的工具。
工程项目不可能基于01编码搭建
管理依赖库
3.2.2 Go的依赖管理
Go的依赖管理演进:GOPATH->Go Vendor->Go Module(广泛应用)
不同环境(项目)依赖的版本不同&&控制依赖库的版本
GOPATH
- go语言支持的环境变量,go项目的工作区
- 包括bin -项目编译的二进制文件,pkg -项目编译的中间产物,加速编译,src -项目源码
- 项目代码直接依赖src下的源码
- go get 下载最新版本的包到src目录下
- 弊端 :项目A和B依赖于同一package的不同版本,但是该package没有前后兼容,因此无法在本地实现package的多版本控制,AB无法同时实现
Go Vendor
- 项目目录下增加vendor文件夹,与main.go同级,vendor存放当前项目依赖的副本
- 项目的依赖会优先从vendor目录获取,若vendor没有,再到GOPATH下寻找
- 通过每个项目引入一份依赖的副本,解决多个项目需要同一个package依赖的冲突问题
- 弊端 :项目A依赖于包B包C,而包BC分别依赖于包D的版本1版本2,这两个版本不兼容。又可能出现依赖冲突,导致编译错误。
Go Mudule 新版本默认开启
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
3.2.3 依赖管理3要素
- 配置文件,描述依赖——go.mod
- 中心仓库管理依赖库——Proxy
- 本地工具——go get/mod
3.2.4 依赖配置-go.mod
三部分组成
①模块路径
②依赖的go的原生库的版本号,不同项目需要的不同
③单元依赖Module Path Version/Pseudo-version ,可以唯一地定位仓库的某个版本或某次提交
3.2.5 依赖配置-version
两种版本规则
①语义化版本${MAJOR}.${MINOR}.${PATCH}
MAJOR:属于大版本,不同MAHOR可以不兼容
MINOR:新增函数等,兼容
PATCH:做代码bug的修复
例:V1.2.0,V1.3.0
②基于commit伪版本xX.0.0-yyyymmddhhmmss-abcdefgh1234
版本前缀:与语义化版本一样
时间戳
哈希码前缀
提交代码时go默认生成伪版本
3.2.6 依赖配置-关键字
indirect :A->B->C,则A对B直接依赖,A对C间接依赖;没有直接导入的模块会有indirect关键字
incompatible :主版本2+模块会在模块路径增加**/vX后缀** 如example/lib5/v3 v3.0.2;没有go.mod文件且主版本2+的依赖,会**+incompatible** 如example/lib6 v3.2.0+incompatible
3.2.7 依赖配置-依赖图
运行版本的算法:选择最低的兼容版本
(已保证1.3 1.4兼容)选择v1.4
3.2.8 依赖分发
去哪下载,如何下载的问题
Ⅰ回源
Github、SVN、···
直接使用版本仓库下载依赖出现的问题:
①无法保证构建稳定性:增加/修改/删除软件版本。例如:之前依赖的版本找不到了
②无法保证依赖可用性:删除软件。例如:作者对软件仓库进行了删除
③增加第三方压力:代码托管平台负载问题。
示意图:
ⅡProxy
是个服务栈,会缓存原栈中的软件内容,缓存的版本不变。若作者删除或修改某个版本,可以保证依赖的稳定性。
直接从GOPROXY拉取依赖
示意图:
Ⅲ变量GOPROXY
GOPROXY是url列表,用逗号分隔,direct表示源站-前面站点都没有依赖的话回源到第三方平台
例:GOPROXY="https://proxy1.cn,https://proxy2.cn,direct" 查找依赖的路径P1->P2->direct
go.mod通过控制GOPROXY的环境变量来控制proxy的配置
3.2.9 工具
go get
go get example.org/pkg默认MAJOR版本的最新
@update 默认
@none 删除依赖
@v1.1.2 tag版本,会拉取特定的语义版本
@23dfdd5 特定的commit
@master 拉取特定的分支,分支的最新commit
go mod
init 初始化项目时需要用到,用来创建go.mod文件 每个项目开始前必须的步骤
download 下载模块到本地缓存,即把所有依赖拉下来
tidy 增加需要的依赖,删除不需要的依赖。每次提交代码时都可以执行该命令,可以减少非必要的依赖,减少整个项目的时间
3.3 测试
类型
①回归测试:终端
②集成测试:对系统功能进行验证
③单元测试:测试开发阶段,开发者对单个函数功能进行测试
覆盖率逐渐增加,但成本逐渐降低。
3.3.1 单元测试
保证质量:新代码不影响旧代码的测试
提升效率:有bug,在本地就可以定位问题
Ⅰ规则
-
所有测试文件以_test.go 结尾
-
测试函数命名规范
func TestXxx(t *testing.T)-
初始化逻辑放到 TestMain 中,如
func TestMain(m *testing.M){ }测试前:数据装载、配置初始化等前置工作
测试后:释放资源等收尾工作
-
Ⅱ例子
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)
}
}
func main(){
}
Ⅲ 运行
Ⅳ assert
使用assert包可以直线equal或not equal的比较,而不用自己写不等符和打印
格式assert.Equal(t,expectOutput,output)
==所有的包需要go get之后import才能使用==
package main
import (
"testing"
"github.com/stretchr/testify/assert" //无法访问--原因是没有go get,但assert写法如下
)
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T){
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t,expectOutput,output)
}
func main(){
}
Ⅴ 覆盖率
go test judgment_test.go judgment.go --cover
计算:代码被执行了多少
提升覆盖率:不同分支的函数
对各个分支进行测试,保证了测试的完备性
Ⅵ Tips
- 一般覆盖率50%~60%保证主流程,较高覆盖率80%
- 测试分支相互独立,全面覆盖
- 测试单元粒度要足够小,则要求函数单一职责
3.3.2 单元测试-依赖
依赖数据库、cache、本地文件
单元测试的目标:幂等&&稳定
幂等:重复运行一个测试时每次结果相同
稳定:单元测试是相互隔离的,单元能在任何时间运行
Ⅰ 文件处理
本地文件,若被修改那么测试文件就失效了
main.go
package main
import (
"bufio"
"os"
"strings"
)
func ReadFirstLine() string {
open, err := os.Open("log") //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
}
main_test.go
package main
import(
"testing"
"assert"
)
func TestProcessFirstLine(t * testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00",firstLine)
}
3.3.3 单元测试-Mock(打桩)
有很多,常用的包 如mockey : github.com/bouk/monkey
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩
- 打桩:用函数A去替换函数B,那么B是原函数,A就是打桩函数
Patch(target, replacement interface{})
target是原函数,目标被替换的函数
replacement是需要打桩的函数
Unpatch(target interface{}) 打桩结束后需要把桩卸载掉
package main
import(
"testing"
"github.com/stretchr/testify/assert"
"bou.ke/monkey"
)
func TestProcessFirstLineWithMock(t * testing.T) {
monkey.Patch(ReadFirstLine, func() string { //对ReadFirstLine打桩测试,不再依赖本地文件
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
firstLine := ProcessFirstLine()
assert.Equal(t, "line000",firstLine)
}
不再依赖本地文件,该测试可以任何时间任何地点进行。
3.3.4 基准测试
- 优化代码,需要对当前代码进行分析
- 内置的测试框架提供了基准测试的能力
- ==代码文件要以_test.go==
- 运行终端命令:
go test -bench=.
Ⅰ例子
并行基准测试 性能有劣化,原因在于rand
package main
import(
"math/rand"
"fmt"
"testing"
)
var ServerIndex [10] int
func InitServerIndex(){
for i := 0; i < 10; i++{
ServerIndex[i] = i+100
}
}
func Select() {
var n = rand.Intn(10)
fmt.Println(ServerIndex[n])
}
func BenchmarkSelect(b *testing.B){
InitServerIndex()//初始化
b.ResetTimer() //定时器的重置,init不属于测试时间之内,因此需要除去
for i := 0; i < b.N; i++{
Select()
}
}
//并行基准测试 性能有劣化,原因在于rand
func BenchmarkSelectParallel(b *testing.B){
InitServerIndex()
b.ResetTimer()
b.RunParallel(func (pb * testing.PB) {
for pb.Next() {
Select()
}
})
}
Ⅱ 优化
?fastrand
3.4 项目实战
项目开发的流程:需求设计-代码开发-测试运行
3.4.1 需求设计
Ⅰ 需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂时不考虑前端页面实现,仅仅实现本地web服务
- 话题和回帖数据用文件存储,不用数据库
Ⅱ 需求用例
浏览消费用户:Topic,PostList
Ⅲ ER图 实体类关系图
一对多的关系
3.4.2 分层结构
- 数据层:关注数据Model,封装外部数据的增删改查
- 逻辑层:业务Entity,处理核心业务逻辑输出,不关心底层数据的存储,service对数据进行封装
- 视图层:视图view,处理和外部的交互逻辑
3.4.3 组件工具
Gin 高性能go web框架 github.com/gin-gonic/g…
Go Mod
创建新的文件夹-终端进入到该项目目录-go mod init- 终端go get gopkg.in/gin-gonic/gin.v1@v1.3.0
3.4.4 Repository
对于两个结构体需要实现基本的查询操作 QueryTopicById 和QueryPostsByParentId
因为有文件,所以可以使用全扫描遍历的方式,一个个对比,类似于MySQL的全面扫描。可以达到目的,但不是最高效的。
Ⅰ 索引
将数据行映射成内存Map,Map时间复杂度是O(1),因此可以很快定位到复杂的数据
两个索引
//两个索引
var (
topicIndexMap map[int64] *Topic
postIndexMap mao[int64][]*Post
)
初始化 话题 数据索引
func initTopicIndexMap(filePath string) error{
open, err := os.Open(filePath+"topic")//打开文件
if err!=nil{
return err
}
scanner := bufio.NewScanner(open)
topicTmpMap := make(map[int64]*Topics)
//迭代器的方式把数据行做一个遍历,转化为结构体,存储到内存Map里
for scanner.Scan(){
text := scanner.Text()
var topic Topic
if err := json.Unmarshal([]byte(text),&topic); err!=nil{
return err
}
topicTmpMap[topic.Id] = &topic //存储到内存map里
}
topicIndexMap = topicTmpMap
return nil
}
初始化 回帖 数据索引
Ⅱ查询
索引:话题ID
数据:话题
索引:话题ID
数据:回帖list
代码完成后就可以给逻辑层
3.4.5 Service
实体
type PageInfo struct{
Topic *repository.Topic
PostList []*reepository.Post
}
流程:参数校验->准备数据->组装实体
运行:go run server.go