这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
1 Go语言进阶
1.1 Goroutine 协程
通过高效的调度模型,实现协程的高并发的操作
1.2 Channel 通道
通过通信实现共享内存
1.3 Sync包的两个关键字
- Lock
- WaitGroup
为了实现并发安全操作和协程间的一些同步
2 依赖管理
2.1 背景 Go依赖管理演进
站在巨人的肩膀上
- 工程不可能基于标准库0~1代码编码搭建
- 管理依赖库
依赖管理演进: GoPATH --> Go Vendor --> GoModule
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
2.1.1 GOPATH
- 环境变量$GOPATH
bin --- 项目编译的二进制文件
pkg --- 项目编译的中间产物,加速编译
src --- 项目源码
- 项目代码直接依赖src下的代码
go get下载最新版本的包到src目录下
弊端:无法实现package的多版本控制。
2.1.2 Go Vendor
- 项目目录下增加 vendor 文件夹,所有依赖包副本放在$ProjectRoot/vendor
- 依赖寻址方式:vendor => GOPATH
优点:通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
弊端:
- 无法控制依赖的版本。
- 更新项目又可能出现依赖冲突,导致编译出错。
2.1.3 Go Module
- 通过
go.mod文件管理依赖包版本 - 通过
go get/go mod指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
2.2 依赖管理三要素
- 配置文件,描述依赖
go.mod - 中心仓库管理依赖库
Proxy - 本地工具
go get/go mod
2.3.1 依赖配置 - go.mod
module github.com/Moonlight-Zhao/go-project-example //依赖管理基本单元
go 1.16 //原生库
require ( //单元依赖
github.com/gin-contrib/sse v0.1.0
github.com/gin-gonic/gin v1.3.0 // indirect
)
依赖标识:[Module Path][Version/Pseudo-version]
2.3.2 依赖配置-version
- 语义化版本
${MAJOR}.${MINOR}.${PATCH}
V1.3.0
V2.3.0
MAJOR:大版本,不兼容,代码隔离
MINOR:新增函数、各种功能,对于一个MAJOR前后兼容
PATCH:代码bug修复
- 基于commit伪版本
vx.0.0-yyyymmddhhmmss-abcdefgh1234
v1.0.0-20200725025532-5a5fe074e612
2.3.3 依赖配置-indirect
A->B->C
- A->B 直接依赖
- A->C 间接依赖 // indirect
2.3.4 依赖配置-incompatible
- 主版本2+模块会在模块路径增加
/vN后缀。 - 对于没有
go.mod文件且主版本2+的依赖,会+incompatible。
2.3.4 依赖配置-依赖图
编译时使用的C项目的版本为v1.4(选择最低的兼容版本)
2.3.5 依赖分发-回源
- 无法保证构建稳定性(增加/修改/删除软件版本)
- 无法保证依赖可用性(删除软件)
- 增加第三方压力(代码托管平台负载问题)
2.3.5 依赖分发-Proxy
2.3.6 依赖分发-变量 GOPROXY
GOPROXY="https://proxy1.cn, https://proxy2.cn, direct"
服务站点URL列表,"direct"表示原站
Proxy 1 ==> Proxy 2 ==> direct
2.3.7 工具-go get
go get example.org/pkg +
@update 默认
@none 删除依赖
@v1.1.2 tag版本,语义版本
@23dfdd5 特定的commit
@master 分支最新的commit
go mod +
init 初始化,创建go.mod文件
download 下载模块到本地缓存
tidy 增加需要的依赖,删除不需要的
3 测试
质量就是“生命” 。
三种类型
- 回归测试:手动通过终端回归一些固定的主流场景。
- 集成测试:系统功能维度自动测试,自动化的回归测试。
- 单元测试:面对测试开发阶段,开发者对单独的函数模块测试验证。
从上到下,覆盖率逐层变大,成本却逐层降低。
3.1 单元测试
3.1.1 单元测试-规则
- 所有测试文件以
_test.go结尾 func TestXxx(*testing.T)- 初始化逻辑放到
TestMain(*testing.M)中
3.1.2 单元测试-例子
main.go:
package main
import "fmt"
func HelloTom() string {
return "Jerry"
}
func main() {
fmt.Println(HelloTom())
}
main_test.go:
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)
}
}
3.1.3 单元测试-运行
go test main_test.go main.go
PS D:\myfiles\WorkSpace\GoWorkPlace> go test main.go main_test.go
--- FAIL: TestHelloTom (0.00s)
main_test.go:9: Expected Tom do not match actual Jerry
FAIL
FAIL command-line-arguments 0.041s
FAIL
3.1.4 单元测试-assert
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
3.1.5 单元测试-覆盖率
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)
}
--cover:测试覆盖率
66.7% :JudgePassLine()中语句只执行到了return true,执行了三分之二。
PS D:\myfiles\WorkSpace\GoWorkPlace> go test .\judgement_test.go .\judgement.go --cover
ok command-line-arguments 0.255s coverage: 66.7% of statements
3.1.5 单元测试-Tips
- 一般覆盖率:50%~60%,较高覆盖率80%+。
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责。
3.2 单元测试-依赖
外部依赖 ==> 稳定&幂等
幂等:重复执行多次结果一样。
稳定:单元测试相互隔离,任何时间任何函数独立运行。
3.3 单元测试-文件处理
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
}
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
3.4 单元测试-Mock
monkey: https://github.com/bouk/monkey
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩
对ReadFirstLine打桩测试,不再依赖本地文件
package main
import (
"bou.ke/monkey"
"github.com/stretchr/testify/assert"
"testing"
)
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)//对桩函数的卸载
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
3.5 基准测试
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
3.5.1 基准测试-例子
package main
import "math/rand"
var ServerIndex [10]int
func InitServerIndex() {
for i := 10; i < 10; i ++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
随机选择执行服务器
3.5.2 基准测试-运行
package main
import "testing"
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
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()
}
})
}
并行进行测试性能劣化:Select使用了sdk的rand函数,为了保证全局的随机性和并发安全,使用了全局锁,降低了性能。
BenchmarkSelect-8 75599122 14.71 ns/op
BenchmarkSelectParallel-8 24799228 46.56 ns/op
3.5.3 基准测试-优化
fastrand:github.com/bytedance/g…
func FastSelect() int {
return ServerIndex[fastrand.Intn(10)]
}
BenchmarkSelectParallel-8 1000000000 0.9790 ns/op
4 项目实践
4.1 需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
4.2 需求用例
浏览消费用户
4.3 ER图-Entity Relationship Diagram
- 话题
- 帖子
4.4 分层结构
- 数据层:数据Model,外部数据的增删改查
- 逻辑层:业务Entity,处理核心业务逻辑输出
- 视图层:试图View,处理和外部的交互逻辑
4.5 组件工具
-
Go Mod
go get -u github.com/gin-gonic/gin
4.6 Repository
两个结构体
- Topic
{
"id":1,
"title":"青训营来啦",
"content":"小姐姐,快到碗里来~",
"create_time":1650437625
}
- Post
{
"id":1,
"parent_id":1,
"content":"小姐姐快来1",
"create_time":1650437616
}
两个基本查询操作
QueryTopicById //通过话题id查询话题
QueryPostByParentId //通过话题id查询话题关联的所有帖子
4.6 Repository-index
元数据 索引
数据行 ==>内存Map
var (
topicIndexMap map[int64]*Topic
postIndexMap map[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]*Topic)
for scanner.Scan() {
text := scanner.Text()
var topic Topic
if err := json.Unmarshal([]byte(text), &topic); err != nil {
return err
}
topicTmpMap[topic.Id] = &topic
}
topicIndexMap = topicTmpMap
return nil
}
初始化帖子数据索引
func initPostIndexMap(filePath string) error{
open, err := os.Open(filePath + "post")
if err != nil {
return err
}
scanner := bufio.NewScanner(open)
postTmpMap := make(map[int64][]*Post)
for scanner.Scan() {
text := scanner.Text()
var post Post
if err := json.Unmarshal([]byte(text), &post); err != nil {
return err
}
posts, ok := postTmpMap[post.ParentId]
if !ok {//如果topic里没有parent
postTmpMap[post.ParentId] = []*Post{&post}//新建一个映射
continue
}
posts = append(posts, &post)//Map里面添加该post
postTmpMap[post.ParentId] = posts//再映射回去
}
postIndexMap = postTmpMap
return nil
}
4.6 Repository-查询
索引:话题ID
数据:话题
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type TopicDao struct {
}
var (
topicDao *TopicDao
topicOnce sync.Once //适合在高并发场景下只执行一次,减少存储浪费
)
func NewTopicDaoInstance() *TopicDao {
topicOnce.Do(
func() {
topicDao = &TopicDao{}
})
return topicDao
}
func (*TopicDao) QueryTopicById(id int64) *Topic {
return topicIndexMap[id]
}
索引:帖子所在话题的id
数据:帖子
type Post struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type PostDao struct {
}
var (
postDao *PostDao
postOnce sync.Once
)
func NewPostDaoInstance() *PostDao {
postOnce.Do(
func() {
postDao = &PostDao{}
})
return postDao
}
func (*PostDao) QueryPostsByParentId(parentId int64) []*Post {
return postIndexMap[parentId]
}
4.7 Service
代码流程编排
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
if err := f.checkParam(); err != nil {
return nil, err
}
if err := f.prepareInfo(); err != nil {
return nil, err
}
if err := f.packPageInfo(); err != nil {
return nil, err
}
return f.pageInfo, nil
}
可用性
func (f *QueryPageInfoFlow) prepareInfo() error {
//获取topic信息
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
f.topic = topic
}()
//获取post列表
go func() {
defer wg.Done()
posts := repository.NewPostDaoInstance().QueryPostsByParentId(f.topicId)
f.posts = posts
}()
wg.Wait()
return nil
}
并行处理话题信息和回帖信息
4.8 Controller
- 构建View对象
- 业务错误码
type PageData struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
func QueryPageInfo(topicIdStr string) *PageData {
topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
pageInfo, err := service.QueryPageInfo(topicId)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}
4.9 Router
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
func main() {
//初始化数据索引
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
//初始化引擎配置
r := gin.Default()
//构建路由
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := cotroller.QueryPageInfo(topicId)
c.JSON(200, data)//json化,将data数据返回
})
//启动服务
err := r.Run()
if err != nil {
return
}
}
func Init(filePath string) error {
if err := repository.Init(filePath); err != nil {
return err
}
return nil
}
4.10 运行
Windows本地ip为127.0.0.1 ,默认端口8080
go run server.go
curl --location --request GET 'http://127.0.0.1:8080/community/page/get/2'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 543 100 543 0 0 60922 0 --:--:-- --:--:-- --:--:-- 67875
{"code":0,"msg":"success","data":{"Topic":{"id":1,"title":"青训营来啦!","content":"小姐姐,快到碗里来~","create_time":1650437625},"PostList":[{"id":1,"parent_id":1,"content":"小姐姐快来1","create_time":1650437616},{"id":2,"parent_id":1,"content":"小姐姐快来2","create_time":1650437617},{"id":3,"parent_id":1,"content":"小姐姐快来3","create_time":1650437618},{"id":4,"parent_id":1,"content":"小姐姐快来4","create_time":1650437619},{"id":5,"parent_id":1,"content":"小姐姐快来5","create_time":1650437620}]}}
5 课后实践
- 支持发布帖子。
- 本地Id生成需要保证不重复、唯一性。
- Append文件,更新索引,注意Map的并发安全问题。