这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
1、语言进阶
1.1 Goroutine(协程)
线程上可以并发地跑多个协程
package concurrence
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) {
defer wg.Done()
hello(j)
}(i)
}
time.Sleep(time.Second)
}
1.2 CSP(Communicating Sequential Processes)
Go中提倡通过 通信共享内存 而不是通过 共享内存实现通信
使用共享内存实现通信,有可能会出现程序竞态的问题,在一定程度上会影响程序的性能
1.3 Channel
make(chan 元素类型, [缓冲大小])
- 无缓冲通道(同步通道) make(chan int)
- 有缓冲通道(典型的C/S模型) make(chan int, 2)
A协程将0-9放入src通道里;B协程从src通道中取出0-9,并将其平方放入dest通道中,主协程打印dest中的数
实现了一个 生产者/消费者 模型
package concurrence
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)
}
}
1.4 并发安全 Lock
5个协程实现1加到2000的操作
import (
"sync"
)
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)
}
1.5 WaitGroup
waitgroup可以实现并发任务的同步
使用waitGroup替换time.Sleep函数
2、依赖管理
2.1 Go 依赖管理演进
GOPATH -> GO Vendor -> Go Module
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
2.1.1 GOPATH
- 环境变量 GOPATH
- 项目源码直接依赖 src 下的代码
- go get 下载最新版本的包到 src 目录下
GOPATH-弊端:对于依赖同一个package的情况下,无法实现package的多版本控制
2.1.2 Go Vendor
- 在项目目录下面增加 vendor 文件,所有依赖包以副本形式放在 $ProjectRoot/vendor
- 依赖寻址方式:vendor => GOPATH
通过每一个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题
Go Vendor-弊端:
- 无法控制依赖的版本
- 更新项目又可能出现依赖冲突,导致编译错误
2.1.3 Go Module
- 通过 go.mod 文件管理依赖包版本
- 通过 go get/go mod 指令工具管理依赖包
它实现了定义版本规则和管理项目依赖关系
2.2 依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
2.3.1 依赖配置-go.mod
如果我们建立的包想要被其他人引用的话,那么需要在每一个包的文件夹下面都建立一个go.mod文件
// 依赖管理基本单元
module github.com/Moonlight-Zhao/go-project-example
// 原生库
go 1.16
// 单元依赖
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.3.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.6 // indirect
)
2.3.2 依赖配置-version(重点)
GOPATH 和 GO Vendor 都是以源码副本的形式实现依赖,没有版本规则的概念。
GO module 为了实现更好的版本管理,定义了一套自己的规则
- 语义化版本
{MINOR}.${PATCH}
不同MAJOR之间是版本隔离的,即不兼容的。MINOR通常是新增函数等,需要保持前后兼容。PATCH一般是做代码bug修复(主要来源于git相关概念)
例子:
V1.3.0
V2.3.0
- 基于 commit 伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh1234
版本前缀(与语义化版本前缀一样)-时间戳(commit的时间戳)-提交commit的12位hash前缀
例子:
v0.0.0-20220401081311-abcdefgh1234
v1.0.0-20201130134442-10cb98257c6c
2.3.3 依赖配置-indirect
Go module 中对于没有直接导入的模块都会标识为非直接依赖,用indirect标识出来
2.3.4 依赖配置-incompatible
go module 中认为 v2 以上的版本需要在前面加上 /vN 的后缀
对于没有go.mod文件并且主版本在 v2 以上的依赖,会+incompatible
2.3.4 依赖配置-依赖图
Go会选择一个最低兼容的版本去编译
2.3.5 依赖分发-回源
常见的依赖是 Github、SVN 等,但是如果直接依赖代码托管平台可能会存在以下问题:
- 无法保证构建稳定性(增加/修改/删除软件版本)
- 无法保证依赖可用性(删除软件)
- 增加第三方压力(代码托管平台负载问题)
2.3.5 依赖分发-Proxy
为了解决上述问题,使用Proxy(没有什么是proxy解决不了的,如果有那就两层proxy)
2.3.6 依赖分发-变量 GOPROXY
go.mod 通过 GOPTOXY 环境变量来实现 goproxy
goproxy是一系列的URL列表,使用逗号分隔。如果proxy中没有的话那就会回源到第三方代码平台上去
GOPROXY="https://proxy1.cn,https://proxt2.cn,direct"
服务站点 URL 列表,“direct”表示源站
2.3.7 工具-go get
若直接 go get 那就会默认拉取最新版本的
2.3.8 工具-go mod
3、测试(从测试的理念出发,提高质量意识)
3.1 单元测试
3.1.1 单元测试-规则
- 所有测试文件以 _test.go 结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到 TestMain 中
3.1.2 单元测试-例子
3.1.3 单元测试-运行
3.1.4 单元测试-assert
3.1.5 单元测试-覆盖率
使用代码覆盖率来评估代码的等级
运行 go test 在结尾加上 --cover 就可以计算出代码的覆盖率
上图中 66.7% 的结果原因是:输入70进去,会走前两行,此时已经返回,则第三行的return false(忽略右大括号)不会被运行,也就只验证了2行正确,第三行无法验证
3.1.5 单元测试-Tips
- 一般覆盖率:50%-60%,则主流程问题不大,对于资金类的交易要求 80%
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
3.2 单元测试-依赖
3.3 单元测试-文件处理
存在问题: 对于文件处理等测试,当文件被修改或者删除后,测试文件也变得无法使用。
3.4 单元测试-Mock
为了解决3.3中出现的问题,可以使用mock进行打桩(Patch 和 Unpatch)
打桩:使用一个函数来替换原本的函数,这样可以让测试文件对代码没有强依赖,在任何环境下执行
3.5 基准测试
Go中提供了测试框架来实现基准测试(例如:对代码进行性能分析)
3.5.1 基准测试-例子
随机选择一个服务器实现负载均衡
3.5.2 基准测试-运行
3.5.3 基准测试-优化
对于随机数的优化可以考虑使用 fastrand
4、项目实战
4.1 需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
4.2 需求用例
4.3 ER 图-Entity Relationship Diagram
4.4 分层结构
- 数据层:数据 Model,外部数据的增删查改
- 逻辑层:业务 Entity,处理核心业务逻辑输出
- 视图层:视图 view,处理和外部的交互逻辑
对于本节课:数据层主要是从本地拉取文件
4.5 组件工具
- Gin 高性能 go web 框架
- Go Mod
-
- go mod init
- go get gopkg.in/gin-gonic/gin.v1@v1.3.0
4.6 Repository
存放依赖数据
Repository-index
Repository-查询
得到:1. 话题ID->话题 2. 话题ID->回帖列表
var (
topicDao *TopicDao
topicOnce sync.Once // 该行实现了单例模式,可以减少内存浪费
)
4.7 Service
考虑到话题和回帖两部分信息是不会相互影响的,因此使用创建子协程的方式来分别实现两个部分
4.8 Controller
- 构建View对象
- 业务错误码
4.9 Router
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
4.10 运行
和客户端的交互,其实就是给客户端一些接口,客户端根据获取的json进行渲染
PS
本文主要作用是作为上课笔记,如有错误,欢迎大家评论,我会及时改正