Go语言入门-工程实践(二)|青训营笔记

Go语言入门-工程实践(二)|青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的的第二篇笔记。

源码:https://github.com/Moonlight-Zhao/go-project-example/tree/V0

本篇的四个部分:

image

Go高性能的本质

并发VS并行

image-20220531185538369

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

并行:多线程程序在多个核的CPU上运行

image-20220531190010147

协程:用户态,轻量级别现成,栈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) 模型

image-20220531192008178

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

channel

image-20220531193749072

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

image-20220531195533824

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

WaitGroup

简单介绍:

image-20220531200116151

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

image-20220531200327458

依赖管理

背景

image-20220531200819901

Go依赖管理演进

image-20220531200852470

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

GOPATH

思路:

image-20220531201015829

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

image-20220531201105292

GO Vendor

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

image-20220531201423643

弊端:

image-20220531201639520

尽管它解决了项目之间的冲突,但是A项目进行更新的时候,如果更新后新包和旧包不兼容,则无法解决。而且没有依赖控制的版本。

GO Module

完美版本:这个可以类比Java中的Maven。

image-20220531201850329

依赖管理三要素

image-20220531202140856

接下来挨个介绍三要素。

依赖配置

go.mod

image-20220531202333933

依赖管理基本单元:引入别的地方的包,也可以是github上项目的包

原生库:可能不同的项目对应的go的版本不同

单元依赖:前面是包名 后面是版本号,这样可以唯一指定。indirect代表非直接依赖。

version

image-20220531202759946

版本控制主要是分两种:

  1. 语义化版本 好像是和git比较像
    MAJOR代表大的版本,可以理解为不同的MAJOR之间是版本不兼容、代码隔离的。
    MINOR代表小的函数增加,这必须在MAJOR兼容的情况下添加功能。
    PATCH代表bug的修复。

  2. 基于commit的伪版本
    版本前缀和语义化版本是一样的。
    中间的是提交commit的时间戳。
    后面的是提交时哈希码的前缀。

indirect

image-20220531203702636

incompatible

image-20220531203934519

当语义版本大于等于2的时候,例如v3.0.2,应当在包名后面跟上v2后缀。

对于没有go.mod文件并且主版本2+的依赖,会在后面加上一个+incompatible,代表可能会有一些不兼容的代码逻辑

依赖图

image-20220531204432919

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

依赖分发

回源

image-20220531204949868

如果我们直接去依赖Github或者SVN这种第三方的源会有以下的问题:

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

image-20220531205331513

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

变量 GOPROXY

image-20220531205631799

首先在第一个网址中查找,没有去第二个,还没有这个direct就是回第三方网站Github上去查找。

下面是查找的路径,Proxy1到Proxy2到Direct,只要有一个有就返回了。

工具

go get

image-20220531205904938

go mod

image-20220531205951156

测试

image-20220531210238026

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

单元测试

image-20220531210549993

保证质量:对当前代码单元测试后发现没问题,再对历史代码进行单元测试也没问题,说明新的代码没有对旧的代码产生影响,因此可以放心的使用。

提升效率:如果出现了bug,可以先运行本地的单元测试,很快的定位问题,而不是需要编译后再去找问题。

规则

image-20220531211205117

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

例子

image-20220531211453091

运行

image-20220531211508705

assert

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

image-20220531211637218

覆盖率

image-20220531211720010

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

image-20220531212153582

覆盖率的计算:因为输入是70,测试函数里的第一行和第二行执行了,第三行没执行。2/3=66.7%。

为了提高覆盖率,我们可以再写一个测试函数:

image-20220531212416580

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

Tips

image-20220531212732883

依赖

image-20220531212920920

幂等:多次运行单元测试,结果应当是一样的。

稳定:单元测试应当是能够相互隔离的,能在任何时间,任何函数进行独立的运行。

文件处理

image-20220531214836289

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

Mock测试

image-20220531215040991

Patch和Unpatch

打桩比较像用一个函数A替代另一个函数B,A就是打桩函数。里面有Patch和Unpatch。

Patch入参有target和replacement,target就是目标要替换掉的函数,B。replacement是打桩函数。

Unpatch就是要把桩卸下来。

实现方式就是在函数运行时,通过Unsafe包,将内存中函数的地址替换成运行时函数地址。最终在测试时调用的其实是打桩函数。

示例

image-20220601101940434

所以其实就是把文件替换成了一个函数罢了。。。这里通过defer实现了对桩函数的卸载。

基准测试

  1. 优化代码,需要的对当前代码分析。
  2. 内置的测试框架提供了基准测试的能力。

例子

image-20220601102350351

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

运行

image-20220601102612547

这里InitServerIndex()是为了初始化服务器索引,初始化后我们需要重置时间,因为这一部分不是测试所花的时间。

为啥多协程并发反而更慢?因为Select()函数用到了Rand包,会加一个全局锁,因此更慢。

优化

image-20220601102900382

将Rand包改成fastrand,牺牲了一部分的随机数的一致性,让测试更快的执行。

项目实战

需求描述

image-20220601103533722

需求用例

image-20220601103625049

Topic就是话题,PostList就是回帖列表。

E-R图

Entity Relationship Diagram

image-20220601103825075

Topic对Post是一对多。

分层结构

image-20220601103913605

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

组件工具

image-20220601104341560

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

image-20220601110536065

Repository

image-20220601110657679

index

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

image-20220601110809779

数据初始化,从文件中读取数据到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
}
流程

image-20220601112315661

参数校验:判断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请求

image-20220601114947593

达成效果。

posted @ 2022-05-31 22:59  杀戒之声  阅读(138)  评论(0编辑  收藏  举报