这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔
前言
课程PPT链接
Go 语言入门 - 工程实践.pptx - 飞书云文档 (feishu.cn)
课程代码链接
GitHub - Moonlight-Zhao/go-project-example
本节课程分为四个部分
- 并发编程
- 依赖管理
- 单元测试
- 项目实战
一、并发编程
-
并发和并行
- 并发是有关结构的,它是一种将一个程序分解成多个小片段并且每个小片段都可以独立执行的程序设计方法;并发程序的小片段之间一般存在通信联系并且通过通信相互协作
- 并行是有关执行的,它表示同时进行一些计算任务
-
Go中的并发类型
- CSP(Communicating Sequential Processes)并发模型:协程之间通过通道Channel实现通信从而共享内存
- 直接通过临界区共享变量实现并发
-
协程 Goroutine
- go并发的执行单元,在函数调用前加关键字go使函数在一个新创建的goroutine中执行,go中主程序在main goroutine中执行,主程序执行结束会打断所有其他goroutine
- 协程与线程的区别:线程一般为内核态,由操作系统内核调度,栈为MB级别;协程为用户态,Go程序中的goroutine由调度器自行调度,栈为KB级别
-
通道 Channel:goroutine间的通信工具,可以通过内置的make()函数创建,ch := make(chan type)
-
接收和发送信息:" <- "符号
-
不带缓存的Channels(同步Channels)
- 发送操作会使发送者goroutine阻塞,直至另一个goroutine在相同的Channels上执行接收操作;同理如果接收先发生也会使…
- 接收者接收到数据发生在发送者再次被唤醒之前
-
单方向的Channels
- chan<- int :表示一个只发送int类型的Channel类
- <-chan int:表示一个只接收…(无法使用close函数)
-
带缓存的Channels:
- 创建:make(chan type , buf)
- 内部持有一个元素队列,只有在队列满时才会阻塞;向缓存Channel发送操作就是向内部缓存队列尾部c插入元素,从缓存Channel接收操作就是从队列头部删除元素
-
-
锁 Lock: pkg.go.dev/sync
-
互斥锁:sync.Mutex
- var mu sync.Mutex
- 通过Lock()和UnLock()函数加解锁;一般通过defer 调用Unlock(),临界区会隐式延伸到函数作用域最后
- 使用Mutex时,应该通过封装确保mutex和其保护的变量没有被导出
-
读写锁(multiple readers, single writer lock):sync.RWMutex
- 通过Rlock()和RUnLock函数加读取锁,Lock()和…加写锁
- 需要更复杂的内部记录,比无竞争锁的mutex慢
-
内存同步问题:由于硬件原因,数据可能以与goroutine写入顺序不同的顺序被提交到主存,所以应该将变量限定在goroutine内部,对于多个goroutine都需要访问的变量使用互斥条件访问
-
-
线程同步 WaitGroup:Go by Example 中文版: WaitGroup (gobyexample-cn.github.io)
二、依赖管理
-
Go依赖管理的演进
-
Go Module 依赖管理方案
-
配置文件,描述依赖:
-
go.mod结构
- 首先模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块,如果是github前缀则表示可以从Github仓库找到该模块,依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的init go.mod文件进行管理
- 下面是依赖的原生sdk版本
- 最下面是单元依赖,每个依赖单元用模块路径+版本来唯一标示
-
version 版本
语义化版本:不同的MAJOR版本表示是不兼容的API,所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块;MINOR版本通常是新增函数或功能,向后兼容;而path版本一般是修复bug
${MAJOR}.${MINOR}. ${PATCH}
V1.3.0
V2.3.0
基于commit伪版本:基础版本前缀是和语义化版本一样的;时间戳(yyyymmddhhmmss,也就是提交Comit的时间,最后是校验码(abocdeabocde ,包含12位的哈希前级,每次提交comit后 Go都会默认生成—个伪版本号
vx.0.0-yyyymmddhhmmss-abcdefgh1234v0.0.0-20220401081311-c38fb59326b7v1.0.0-20201130134442-10cb98267c6c
-
特殊标识符
- 最常见的indirect后缀,表示go.mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,标示间接依赖
- 下一个常见是的是incompatible,主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。由于gomodule是1.1实验性引人所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有g.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible后缀
-
依赖选择:Go会根据算法选择最低兼容版本
-
-
依赖分发:Proxy——Go Proxy是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件出除之后依然可用,从而实现了供 "immutability"和"available"的依赖分发;使用Go Proxy之后,构建时会直接从 Go Proxy 站点拉取依赖
(推荐设置七牛云 - Goproxy.cn代理,按照页面说明操作即可)
-
依赖工具:go get/mod
-
go get
-
go mod
-
-
三、测试
阅读PPT即可,不需特意记忆,使用时可通过go help test 或go help testflag来查看命令参数
-
单元测试
-
规则:
-
assert在单元测试中的使用
-
测试覆盖率
-
依赖:工程中复杂的项目,一般依赖不够稳定,而我们的单测需要保证稳定性和幂等性,稳定是指相互隔离,能在任何时间,任何环境,运行视试。,幂等是指每一次测枇运行都应该产生与之前一样的结果。要实现这一目的就要用到mock机制。
-
mock测试:针对IO相关的应用,常规的测试方法会受到测试文件数据等的变化的限制,没办法保持一致性,所以引入mock测试
-
-
基准测试:测量一个程序在固定工作负载下的性能,与普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数
四、项目实践
利用提供的代码,针对一个发帖评论页面的后端实现进行需求分析、代码开发、测试运行整个项目流程的详细介绍
获取演示代码
git clone --branch V0 https://github.com/Moonlight-Zhao/go-project-example.git
需求设计
-
需求描述
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
-
需求用例
-
E-R图:概念模型描述
-
分层结构:
- 数据层关联底层数据模型model,封装外部数据的增删改查,将数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层是,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的(DAO(Data Access Object)模式)
- Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;
- Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,封装json格式化的请求结果以api形式访问
-
组件工具
- Gin:Go Gin 简明教程 | 快速入门 | 极客兔兔 (geektutu.com)
- go mod
Repository
-
model实现
// topic.go type Topic struct { Id int64 `json:"id"` Title string `json:"title"` Content string `json:"content"` CreateTime int64 `json:"create_time"` } // post.go type Post struct { Id int64 `json:"id"` ParentId int64 `json:"parent_id"` Content string `json:"content"` CreateTime int64 `json:"create_time"` } -
使用索引Map加快查找——db_init.go
var ( topicIndexMap map[int64]*Topic postIndexMap map[int64][]*Post ) //通过索引Map加快查找 func Init(filePath string) error { //对索引进行初始化 if err := initTopicIndexMap(filePath); err != nil { return err } if err := initPostIndexMap(filePath); err != nil { return err } return nil } func initTopicIndexMap(filePath string) error { open, err := os.Open(filePath + "topic") //打开文件 if err != nil { return err } scanner := bufio.NewScanner(open) //转换为scanner 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 } //完成topicIndexMap的初始化 -
查找实现
var ( topicDao *TopicDao//提供供上层访问的Dao对象 topicOnce sync.Once //适用于高并发下的一次性初始化,单例模式 ) func NewTopicDaoInstance() *TopicDao { topicOnce.Do( func() { topicDao = &TopicDao{} }) return topicDao } func (*TopicDao) QueryTopicById(id int64) *Topic { return topicIndexMap[id] } //通过索引直接利用id进行查询
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//使用WaitGroup实现等待多个goroutine退出 wg.Add(2) go func() { defer wg.Done() topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId) f.topic = topic }()
Controller
构建View对象(返回JSON数据)和生成错误码(通过特定错误码表示业务中错误)
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,
}
}
Rounter
server.go,Gin框架Rounter的简单使用
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)
})
err := r.Run()
if err != nil {
return
}
}
运行展示
在浏览器访问对应url,可以查看到返回的JSON数据
在控制台端口查看Server端的输出