Go语言入门-工程实践(二)|青训营笔记
Go语言入门-工程实践(二)|青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第二篇笔记。
源码:https://github.com/Moonlight-Zhao/go-project-example/tree/V0
本篇的四个部分:

Go高性能的本质
并发VS并行

并发:一个核的CPU上 间歇的运行多线程程序
并行:多线程程序在多个核的CPU上运行

协程:用户态,轻量级别现成,栈KB级别
线程:内核态,线程跑多个协程,栈MB级别
简单实例:
package main
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) { //新建一个协程
			hello(j) //每个协程输出一下
		}(i)
	}
	time.Sleep(time.Second)//启动5个协程后  主进程停一秒  让协程们执行完
}
func main() {
	HelloGoRoutine()
}
/*
hello goroutine : 2
hello goroutine : 3
hello goroutine : 4
hello goroutine : 0
hello goroutine : 1
*/
CSP(Communicating Sequential Processes) 模型

GO语言提倡通过通信共享内存,而不是通过共享内存而实现通信。这里可以看这篇文章来稍微了解一下Golang CSP并发模型。
channel

channel分成有缓冲和无缓冲两种。
生产者和消费者模型简单实例:
package main
func CalSquare() {
	src := make(chan int)
	dest := make(chan int, 3)
	go func() { //生产者 往src里添加数据
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()
	go func() { //消费者 从src中取出数据存入dest
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()
	for i := range dest { //消费者从dest里进行消费  因为生产者的速度可能会比消费者的快很多,因此dest增加了缓冲区
		//复杂操作
		println(i)
	}
}
func main() {
	CalSquare()
}
/* 结果:
0
1
4
9
16
25
36
49
64
81
*/
并发安全 Lock

这里不知道为啥跑官方的代码我加锁不加锁是一个样子的,很奇怪,可能偶尔会出错吧。。。。
WaitGroup
简单介绍:

通过WaitGroup重现 并发VS并行中的示例:

依赖管理
背景

Go依赖管理演进

现在主要在用的就是Go Module,要从头开始讲起
GOPATH
思路:

缺陷:A项目依赖V1版本,B项目依赖V2版本,无法实现package多版本控制

GO Vendor
解决了GoPath的问题,A项目和B项目先去自己的vendor文件中寻找依赖包,这样项目与项目之间就不会起冲突。

弊端:

尽管它解决了项目之间的冲突,但是A项目进行更新的时候,如果更新后新包和旧包不兼容,则无法解决。而且没有依赖控制的版本。
GO Module
完美版本:这个可以类比Java中的Maven。

依赖管理三要素

接下来挨个介绍三要素。
依赖配置
go.mod

依赖管理基本单元:引入别的地方的包,也可以是github上项目的包
原生库:可能不同的项目对应的go的版本不同
单元依赖:前面是包名 后面是版本号,这样可以唯一指定。indirect代表非直接依赖。
version

版本控制主要是分两种:
- 
语义化版本 好像是和git比较像 
 MAJOR代表大的版本,可以理解为不同的MAJOR之间是版本不兼容、代码隔离的。
 MINOR代表小的函数增加,这必须在MAJOR兼容的情况下添加功能。
 PATCH代表bug的修复。
- 
基于commit的伪版本 
 版本前缀和语义化版本是一样的。
 中间的是提交commit的时间戳。
 后面的是提交时哈希码的前缀。
indirect

incompatible

当语义版本大于等于2的时候,例如v3.0.2,应当在包名后面跟上v2后缀。
对于没有go.mod文件并且主版本2+的依赖,会在后面加上一个+incompatible,代表可能会有一些不兼容的代码逻辑。
依赖图

这道题我在第一次听的时候选的C,现在看作为一个只是版本管理的系统,不会去智能的选择两个。而且1.3和1.4是相互兼容的,因此我们应当在满足所有项目的要求的情况下选择最低版本,即1.4,即便是有1.5也是选择1.4。我第二次选择了A...很尬,选了1.3那B项目直接报错了。。
依赖分发
回源

如果我们直接去依赖Github或者SVN这种第三方的源会有以下的问题:
- 无法保证构建稳定性,因为作者可以随时随地的增加或者删除自己的代码。
- 无法保证依赖可用性,因为作者删了 你就没得用了,项目就崩溃了。
- 增加第三方压力,Github本来是没想着把项目这么用的,直接依赖会导致高并发的访问,给他们带来巨大压力并违背初衷。
Proxy

我们可以将代码管理到Proxy中,这样就直接从Proxy中下载和访问,稳定且可靠,解决了上面的问题。
变量 GOPROXY

首先在第一个网址中查找,没有去第二个,还没有这个direct就是回第三方网站Github上去查找。
下面是查找的路径,Proxy1到Proxy2到Direct,只要有一个有就返回了。
工具
go get

go mod

测试

- 回归测试:测试人员手动的去点击,使用程序的功能,看是否符合预期。
- 集成测试:对系统功能维度进行验证,做一些自动化的回归测试。
- 单元测试:在一定程度上决定代码的质量,所以很重要。
单元测试

保证质量:对当前代码单元测试后发现没问题,再对历史代码进行单元测试也没问题,说明新的代码没有对旧的代码产生影响,因此可以放心的使用。
提升效率:如果出现了bug,可以先运行本地的单元测试,很快的定位问题,而不是需要编译后再去找问题。
规则

- 测试文件的命名以_test.go结尾。
- 函数命名要Test+驼峰规则。
- TestMain函数在更高维度上抽象了,初始化和释放资源可以在里面。m.Run()可以运行所有的单元测试。
例子

运行

assert
上面的例子中直接使用了不等的符号,assert包里面有很多判断相等的函数,可以用这个避免有些想的不全。

覆盖率

测试的好坏:代码覆盖率越高越好。

覆盖率的计算:因为输入是70,测试函数里的第一行和第二行执行了,第三行没执行。2/3=66.7%。
为了提高覆盖率,我们可以再写一个测试函数:

这样测试的覆盖率就是100%了。
Tips

依赖

幂等:多次运行单元测试,结果应当是一样的。
稳定:单元测试应当是能够相互隔离的,能在任何时间,任何函数进行独立的运行。
文件处理

如果这个单元测试所测试的文件被别人删除或者修改,就会丢失幂等和稳定性,接下来引入Mock测试。
Mock测试

Patch和Unpatch
打桩比较像用一个函数A替代另一个函数B,A就是打桩函数。里面有Patch和Unpatch。
Patch入参有target和replacement,target就是目标要替换掉的函数,B。replacement是打桩函数。
Unpatch就是要把桩卸下来。
实现方式就是在函数运行时,通过Unsafe包,将内存中函数的地址替换成运行时函数地址。最终在测试时调用的其实是打桩函数。
示例
所以其实就是把文件替换成了一个函数罢了。。。这里通过defer实现了对桩函数的卸载。
基准测试
- 优化代码,需要的对当前代码分析。
- 内置的测试框架提供了基准测试的能力。
例子

这里我们要对Select()函数做基准测试。
运行

这里InitServerIndex()是为了初始化服务器索引,初始化后我们需要重置时间,因为这一部分不是测试所花的时间。
为啥多协程并发反而更慢?因为Select()函数用到了Rand包,会加一个全局锁,因此更慢。
优化

将Rand包改成fastrand,牺牲了一部分的随机数的一致性,让测试更快的执行。
项目实战
需求描述

需求用例

Topic就是话题,PostList就是回帖列表。
E-R图
Entity Relationship Diagram

Topic对Post是一对多。
分层结构

- 数据层:数据Model,外部数据的增删改查。Service不需要管数据层Repository如何实现,只需要关心Model的数据就行。
- 逻辑层:业务Entity,处理核心业务逻辑输出。组装数据层传过来的数据,例如订单需要很多信息去组合。
- 视图层:视图view,处理和外部的交互逻辑。这里更多的是和上层去交互,json格式化一些结果,封装一些API。
组件工具

运行go get这个命令,添加gin依赖。这个gopkg.in网址可以打开看一看,里面版本是和github对应的。

Repository

index
可以考虑把数据放入到map中,来获取O(1)的查找。

数据初始化,从文件中读取数据到map中,并把字符反序列化成对象。
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)//创建map key是int value是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//map中id:对象地址
	}
	topicIndexMap = topicTmpMap
	return nil
}
查询
package repository
import (
	"sync"
)
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 {//因为这个包在repository下,因此这个函数可以通过repository.NewPostDaoInstance()来调用  返回一个PostDao
	postOnce.Do(
		func() {//只初始化一个PostDao
			postDao = &PostDao{}
		})
	return postDao
}
func (*PostDao) QueryPostsByParentId(parentId int64) []*Post {//这个函数属于PostDao对象
	return postIndexMap[parentId]
}
这里的索引是话题ID,数据是话题。而我们有一个功能是话题下很多个帖子,索引:话题ID,数据是帖子列表,这就需要在逻辑层进行组装。
Service
实体
type PageInfo struct {
	Topic    *repository.Topic
	PostList []*repository.Post
}
流程

参数校验:判断id是否合法,不能为0之类的。
代码流程编排
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
}
这里的代码串联起了开始的知识,通过WaitGroup并行读取两种数据,并且这两种操作必须是没有关联的,不然会产生冲突,感觉是多协程的经典案例。
Controller
构建View对象,定义业务错误码。
package cotroller
import (
	"strconv"
	"github.com/Moonlight-Zhao/go-project-example/service"
)
type PageData struct {//构建View对象
	Code int64       `json:"code"`//定义业务错误码
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}
func QueryPageInfo(topicIdStr string) *PageData {
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)//这里是传入ID为字符串,转成Int类型
	if err != nil {
		return &PageData{
			Code: -1,//如果转int失败返回-1
			Msg:  err.Error(),
		}
	}
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	return &PageData{
		Code: 0,//成功返回0
		Msg:  "success",
		Data: pageInfo,
	}
}
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)
	})
	err := r.Run()
	if err != nil {
		return
	}
}
运行效果
go run .\server.go 运行项目
通过Postman发送Get请求

达成效果。


 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号