今天是青训营伴学笔记创作的第三天!
下面将介绍Go语言进阶的相关内容,具体可以包含以下几项 :
- 并发编程
- 依赖管理
- Go依赖管理演进
- 测试
- 项目实践
并发编程
并发 vs 并行
Go可以充分发挥多核优势,高效运行
Goroutine
协程 : 用户态,轻量级线程,栈KB级别
线程 : 内核态 ,线程跑多个协程,栈MB级别
go开启协程 : 只需要在函数前面加上一个go关键字
例 : 快速打印hello goroutine 0-4 (快速 : 开多个协程去打印)
package main
import (
"fmt"
"time"
)
func hello(i int) {
fmt.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()
}
函数 HelloGoRoutine 包含一个 for 循环,它会创建 5 个 goroutine 并发执行。每个 goroutine 都会调用一个匿名函数,并传递一个整数参数 j。在匿名函数中,它会调用 hello(j) 函数。
使用 go 关键字可以将一个函数调用包装为一个 goroutine,使其在独立的执行线程中运行。对于循环中的每个迭代,使用匿名函数和参数 i 创建一个新的 goroutine。
这种方式可以实现并发执行,但不保证多个 goroutine 的执行顺序。因此,hello(j) 函数的执行顺序可能是随机的。
为了等待所有 goroutine 执行完毕,代码使用 time.Sleep(time.Second) 来暂停主 goroutine 的执行 1 秒钟。这样做是为了确保所有创建的 goroutine 有足够的时间完成执行。
CSP
- CSP(Communicating Sequential Processes)是一种并发计算的模型和理论
- go提倡通过通信共享内存而不是通过共享内存而实现通信
Channel
Channel是一种引用类型
语法 :
make(chan 元素类型,[缓冲大小])
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int ,2)
package main
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
// 复杂操作
println(i)
}
}
func main() {
CalSquare()
}
并发安全 Lock
WaitGroup
sync.WaitGroup 类型是用于等待一组 goroutine 完成执行的同步原语。WaitGroup 提供了一种简单而有效的方法来等待并发操作的完成。
sync.WaitGroup 提供了三个主要的方法:
Add(delta int):向WaitGroup中添加 delta 值,代表需要等待的 goroutine 的数量。每次启动一个新的 goroutine 时,应该调用Add(1)来增加等待的数量。Done():通知WaitGroup一个已完成的 goroutine。每个 goroutine 在执行完任务后,应调用Done()来通知WaitGroup完成一个任务。Wait():阻塞运行的 goroutine,直到WaitGroup中的计数器归零。当等待的所有任务都完成时,Wait()返回,允许程序继续执行。
以下是一个示例,演示如何使用 sync.WaitGroup:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 在函数结束时通知 WaitGroup,完成一个任务
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // 模拟工作耗时
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup // 创建一个 WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加等待的任务数量
go worker(i, &wg)
}
wg.Wait() // 等待所有任务完成
fmt.Println("All workers done")
}
在上述示例中,worker 函数模拟一个具体的工作任务。在每个 goroutine 的开始和结束处,使用 wg.Done() 来通知 WaitGroup 完成一个任务。
在 main 函数中,我们创建了一个 WaitGroup 对象 wg。然后,启动了三个 goroutine,每个 goroutine 执行一个 worker 任务,并通过传递 &wg 来让 worker 函数使用同一个 WaitGroup 对象。
最后,调用 wg.Wait() 等待所有的任务完成。一旦所有的任务都完成,主 goroutine 继续执行并打印 “All workers done”。
使用 sync.WaitGroup 可以方便地等待一组 goroutine 的完成,保证了并发操作的同步和顺序。
hello goroutine 0-4 由 WaitGroup 改写
package main
import (
"fmt"
"sync"
)
func hello(i int) {
fmt.Println("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoRoutine() {
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() {
HelloGoRoutine()
}
依赖管理
背景
- 工程项目不可能基于标准库0~1编码搭建
- 管理依赖库
Go依赖管理演进
GOPATH -> Go Vendor -> Go Module
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
GOPATH
GOPATH 是 GO 语言支持的一个环境变量
- src : 存放go项目的地方
- pkg : 存放编译的中间产物,加快编译速度
- bin : 存放go项目编译生成的二进制文件
xx :
- 项目代码直接依赖src下的代码
- go get 下载最新版本的包到src目录下
GOPATH-弊端
如图:
同一个pkg,有2个版本,A->A(),B->B(),而src下只能有一个版本存在,那AB项目无法保证都能编译通过,也就是在gopath管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,govender出现了。
Go Vendor
Vendor是当前项目中的一个目录,其中存放了当前项目依赖的副本。在Vendor机制下,如果当前项目存在vendor目录,会优先使用该目录下的依赖,如果依较不存在,会从GOPATH中寻找;这样***。
- 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式 : vender => GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
但vendor无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题,下面我们看一个场景。
Go Vendor - 弊端
问题 :
- 无法控制依赖的版本
- 更新项目可能又出现依赖,导致编译出错
Go Module
- 通过go.mod 文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
终极目标 : 定义规则和管理项目的依赖关系
依赖管理三要素
类比java中的Maven
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get / mod
依赖配置 - go.mod
依赖标识 :
[Module Path] [Version/Pseudo-version]
- 首先模块路径用来标识一个模块,从模块路径可以看出从哪里找到孩模块,如果是gthub前缀则表示可以从cithub仓库找到孩模块,依赖包的源代码由gthub托管,如果项目的子包想被单独引用,则需要通过单独的init go。mod文件进行管理。
- 下面是依赖的原生sdk版本
- 最下面是单元依赖,每个依赖单元用模块路径+版本来唯─标示。
依赖配置 - 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 -
gopath和govendor都是源码副本方式依赖,没有版本规则概念,而gomod为了放方便管理则定义了版本规则,分为语义化版本和"*;其中语义化版本包括,不同的MAJOR版本表示是不兼容的API,所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块;MINOR版本通常是新增函擞或功能,向后兼容;而patcth版本一般是修复bug;而基于commit的为版本包括"**,基础版本前缀是和语义化版本一样的;时间戳(w.ymmddhmssl,也就是提交Commit的时间,最后是校验码(abcdefabcdef ,包含12位的哈希前缀;每次提交commit后Go 都会默认生成一个伪版本号。
测试
测试是避免事故的最后一道屏障
类型 :
-
回归测试
-
集成测试
-
单元测试
单元测试规则
- 所有测试文件以_tset.go结尾
- func TextXxx(*testing.T)
- 初始化逻辑放到TestMain中
测试例子
package main
import (
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
func HelloTom() string {
return "Jerry"
}
- 在vscode中在终端输入go test就好了
- 在goland中直接右键运行就好了
效果 :
单元测试-assert
assert函数代替了之前的Errorf,用于测试文件方便
问题 :
解决 :
go get github.com/stretchr/testify/assert
其它问题 :
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t,true,isPass)
}
单元测试-代码覆盖率
judgement_test.go :
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
judgement.go :
package main
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
运行(vscode) :
go test judgement_test.go judgement.go --cover
效果截图 :
66.7的计算 :
在judgement.go中。有效代码(3行)为 :
if score >= 60 {
return true
}
return false
在单元测试中,单元测试执行了两行,故2/3=66.7%
如果再添加一个不及格的case,如 :
func TestJudgePassLineFalse(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
}
或者将judgement中的代码改为 :
return score >= 60
代码的覆盖率将达到100%
单元测试 -- tips
- 一般覆盖率 : 50-60%,较高覆盖率 : 80%以上
- 测试分支相互独立,全面覆盖
- 测试单元粒度足够小,函数单一职责
单元测试 -- 依赖
工程中复杂的项目,一般会依赖数据库,本地文件,cache等等,单侧需要保持稳定性和幂等性。
- 稳定 : 相互隔离,能在任何时间,任何环境,运行测试
- 幂等性 : 每一次测试运行都应该产生与之前一样的结果,而要实现这一目的就要用到mock机制
单元测试 -- 文件处理
对本地文本测试简例 :
log (文本文件):
line11
line22
line33
line44
wj.go : (读取文本文件的第一行)
package main
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
}
wj_test.go
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
在终端输入 :
go test .\go_JingJie\WenJianChuLi_test\wj_test.go .\go_JingJie\WenJianChuLi_test\wj.go
完成wj_test.go对wj.go的测试
实现截图 :
在这个例子中,将文件中的第一行字符中的11换成00,执行单例,测试通过。
但是这个单测需要依赖于本地文件,如果本地文件被修改或者删除就会fail。
为例保证测试case的稳定性,我们对读取文件进行mock,屏蔽对文件的依赖
单元测试 -- Mock
monkey : github.com/bouk/monkey
monkey是一个开源的mock测试库,可以对method,或则实例方法进行mock,反射,指针赋值。
快速 Mock 函数
- 为一个函数打桩
- 为一个方法打桩
mock包获取 :
go get bouk.ke/monkey
go get 命令是一个方便的工具,用于获取并安装 Go 语言的包、库或命令行工具。
将之前的wj_test.go代码换成 :
package main
import (
"bou.ke/monkey"
"github.com/stretchr/testify/assert"
"testing"
)
func TestProcessFirstLineWithMock(t *testing.T) {
// ReadFirstLine 函数被替换为一个匿名函数 func() string { return "line110" };
// 该匿名函数总是返回字符串 “line110”
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)// 通过defer卸载mock
firstLine := ProcessFirstLine()
assert.Equal(t, "line000", firstLine)
}
这样对ReadFirstLine打桩测试,就不会再依赖本地文件
然后测试,就成功了。
基准测试
go语言提供了基准测试框架
- 优化代码,需对当前代码分析
- 内置的测试框架提供了基准测试的能力
例子 :随机选择执行服务器
import "math/rand"
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
- 基准测试以Benchmark开头,入参是testing.B,用b中的N值反复递增循环测试
- (对一个测试用例的默认测试时间是1秒,当测试用例函数返回时还不到1秒,那么testingB中的N值将按1、2、5、10、20、.…递.增,并以递增后的值重新进行用例函数测试。)
- Resttimer重置计时器,我们再reset之前做了init或其他的准备操作,这些操作不应该作为基准刻试的范围;numprale是多协程并发测试;执行2个基准知过,发现代码在并发情况下存在劣化,主要原因是rand为了保证全局的随机性和并发安全,持有了一把全局锁。
测试代码 :
package main
import "testing"
func BenchmarkSelect(b *testing.B) {
InitServerIndex() // init服务器列表
b.ResetTimer() // 定时器重置
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
运行结果 :
基准测试优化 : fastrand()
项目实践
社区话题页面 :
- 展示话题(标题,文字描述) 和 回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题 和 回帖数据用文件存储
需求用例
ER图
- 话题 : Topic
- 帖子 : Post
分层结构
- 数据层 : 数据Model,外部数据的增删改查
- 逻辑层 : 业务Entity,处理核心业务逻辑输出
- 视图层 : 视图view,处理和外部的交互逻辑
组件工具
-
Gin高性能 go web框架 : github.com/gin-gonic/g…
-
Go Mod
- go mod init
- go get gopkg.in/gin-gonic/gin.v1@v1.3.0