Go语言上手-工程实践 | 青训营笔记

144 阅读7分钟

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

并发

并发 VS 并行

并发:多线程程序在一个核的cpu上运行。通过时间片的切换实现 并行:直接利用多个核的cpu分别运行。

Go可以充分发挥多核优势,高效运行。

1.1 Goroutine 协程

线程:昂贵的系统资源,内核态,创建切换停止都属于很重的系统操作,消耗资源,栈MB级别。 协程:用户态,轻量级线程,创建和调度由Go完成,栈KB级别。 一个线程可以有多个协程。go可以同时开启上万个协程。

协程创建方式: go func

例子:快速打印 hello goroutine:0~hello goroutine:4

func hello(i int) {
	println("hello world : " + fmt.Sprint(i))
}

func helloGoroutine() {
	for i := 0; i < 5; i++ {
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)//防止主协程在子协程结束之前结束,阻塞一下
}

1.2 Goroutine协程之间的通信 CSP(communication sequential processes)

go提倡通过通信共享内存而不是通过共享内存而实现通信,从而实现高并发。如何理解?通信利用了通道Channel。通道将协程之间连接,像遵循先入先出传输队列,完成数据的传输。通道是让一个goroutine发送特定值到另一个goroutine的机制。 go也保留了通过共享内存实现通信的实现,存在一个临界区,各个goroutine来访问,若同时访问存在数据竞态的问题。

image.png

1.3 通道 Channel

是一种引用数据类型。 make(chan 元素类型,[缓冲大小]) 分为:1.无缓冲通道meke(chan int)和 2.有缓冲通道make(chan int, 2)

image.png

无缓冲通道导致发送和接收的goroutine的同步化,因此又叫同步通道。

使用带有缓冲区的有缓冲通道可以解决同步问题。比如,生产消费问题,生产速度和消费速度不一致,使用带缓冲区的channel可以解决这一问题

通过通信实现共享内存例子: A子协程发送0-9数字,B子协程计算输入数字的平方,主协程输出最后的平方数

package concurrence

func CalSquare() {
	src := make(chan int)
	dest := make(chan int, 3)
	go func() { //一个协程A
		defer close(src)
		for i := 0; i < 10; i++ { //把从0到9的整数放进src这个channel
			src <- i
		}
	}()
	go func() { //一个协程B
		defer close(dest)
		for i := range src { //把src中的整数依次取出,平方后放入dest的channel
			dest <- i * i
		}
	}()
	for i := range dest { //打印结果
		println(i)
	}
}

1.4 并发安全 Lock

Lock是sync包内的关键字之一。

这个例子是通过共享内存实现通信。由图可知,加lock和不加lock的情况结果不同。不加锁的执行情况是不确定的,概率出现错误。所以在开发中应避免对共享内存进行非并发安全的读写操作。 image.png

1.5 WaitGroup

在sync包内。WaitGroup可以实现并发任务的同步。里面有一个计数器,三个方法:Add(delta int)计数器+delta(开启协程+1),Done()计数器-1(执行结束-1), Wait()主协程阻塞直到计数器为0(所有并发任务都已完成)

用WaitGroup优化1.1例子中调用的sleep(sleep不能准确直到子协程需要的时间,不够优雅)

func ManyGoWait(){
    var wg sync.WaitGroup
    wq.Add(5)
    for i:=0; i<5; i++{
        go func(j int){
            //wg.Add(1)
            defer wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}

2.依赖管理

依赖是指各种开发组件和工具,我们用它们来提高工作效率,把重点放在工程业务的实现逻辑,而不是0基于标准哭的0和1的构建。

2.1 Go依赖管理演进

GOPATH >> Go Vendor >> Go Module

2.1.1 GOPATH

go项目的环境变量-工作区——该目录下有三个文件夹 bin pkg src

  • bin: 项目编译的二进制文件
  • pkg: 项目编译的中间产物,加速编译
  • src: 项目源码 GOPATH中所有项目的代码,依赖包都在src中,用go get下载最新版本的包到src目录下

弊端:若A和B依赖于某一package的不同版本,无法实现package的多版本控制,新的取代旧的。

2.1.2 Go Vendor

每个项目的目录下增加vendor文件,所有依赖包以一个副本形式放在vendor中,解决了多个项目需要同一个package依赖的冲突问题。

弊端:无法控制依赖版本,更新项目后可能出现依赖冲突,导致编译出错。

image.png

2.1.3 Go Module

通过go.mod文件管理依赖包版本 通过 go get/ go mod 指令工具管理依赖包

(1)go mod 命令

    download    download modules to local cache
    edit        edit go.mod from tools or scripts
    graph       print module requirement graph
    init        initialize new module in current directory
    tidy        add missing and remove unused modules
    vendor      make vendored copy of dependencies
    verify      verify dependencies have expected content
    why         explain why packages or modules are needed

(2)go mod 环境变量

2.2 依赖管理三要素

  1. 配置文件 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/mod

2.3 依赖配置

2.3.1 go.mod

go.mod 就是如下图所示这样的文件,是每个项目都有一个的配置文件。

module example/project/app  //依赖管理基本单元

go 1.16 //go的原生库的版本

require (//单元依赖:【Module Path】【一个仓库的版本/提交号version/Pseudo-version】
        github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/gin-gonic/gin v1.3.0 // indirect
	github.com/ugorji/go v1.2.7 // indirect
	go.uber.org/atomic v1.9.0 // indirect
	go.uber.org/multierr v1.8.0 // indirect
	go.uber.org/zap v1.21.0 // indirect
	golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
	golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
	google.golang.org/protobuf v1.28.0 // indirect
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
	gopkg.in/gin-gonic/gin.v1 v1.3.0
	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
	gorm.io/driver/mysql v1.3.3 // indirect
	gorm.io/gorm v1.23.4 // indirect
)

2.3.2 version

image.png

语义化版本的三个部分:major 大版本 互相不兼容,major 小版本 功能增加, patch 代码补丁 伪版本三个部分: 语义化版本-时间戳-commit的hash码的前12位 校验码

2.3.3 indirect

go.mod中的一个关键字,表示非直接导入的依赖。比如a依赖b,b依赖c,a对c标注indirect

2.3.4 incompatible & 依赖图

标识:可能存在不兼容的代码逻辑

image.png

image.png 选b!!选择最低的兼容版本,要是有v1.5就选1.5了。

2.3.5 依赖分发-回源

若开发者修改Github,SVN项目,依赖此项目的人将找不到原来依赖的版本-无法保证构建稳定性,依赖可用性。 增加第三方压力,若很多人依赖同一代码,代码托管平台负载过重。为解决这个问题,看2.3.6

2.3.6 依赖分发 - Proxy

项目第三方依赖库的下载源地址,用于设置Go模块代理,使go在拉取模块版本时直接通过镜像站点快速拉取。(缓存每个版本的文件内容的站点。)稳定+可靠
配置环境变量GOPROXY
通过go env -w GOPROXY=https://goproxy.cn,direct设置
GOPROXY="proxy1.cn, proxy2.cn, direct" 里面写的是服务站点URL列表,还有一个“direct”表示源站,先按次序在列表中找,如果服务站点都找不到,就去模块版本的源地址抓取,比如Github等(回源)。
默认值:GOPROXY=proxy.golang.org,direct
国内不用fq的镜像:七牛云 GOPROXY=goproxy.cn,direct 阿里云(没贴地址)

2.3.7 工具-go get

go get example.org/pkg //默认添加major最新的版本 后面加上

  • @update 默认 等于不加
  • @none 删除依赖
  • @v1.1.2 tag版本,语义版本
  • @23dfdd5特定的commit
  • @master 分支的最新commit

2.3.8 工具-go mod

以下为go mod 所有方法,在cmd输入go mod help 可查询到。常用的有 go mode init/go mod tidy/ go mod vendor...

    download    download modules to local cache
    edit        edit go.mod from tools or scripts
    graph       print module requirement graph
    init        initialize new module in current directory
    tidy        add missing and remove unused modules
    vendor      make vendored copy of dependencies
    verify      verify dependencies have expected content
    why         explain why packages or modules are needed

2.3.9 环境变量 go env

可以通过go env来查看环境变量的设置

  • GO111MODULE Go语言1.11版本后有了go module, 用这个环境变量来作为Go modules的开关。
    auto: 只要项目包含go.mod文件,就启用Go modules.
    on:启用Go modules
    off:关闭Go modules
    可以在cmd通过go env -w GO111MODULE=on来设置
  • GOPROXY 上面有专门讲哦
  • GOSUMDB 用来校验拉取的第三方库是否是完整的。默认是国外的网站,但是如果设置了goproxy,就不用设置。
  • GONOPROXY,GONOSUMDB,GOPRIVATE 通过设置goprivate即可,go env -w GOPRIVATE="xx,xx,xx"表示xx都是私有仓库,不会进行GOPROXY下载和校验。go env -w GOPRIVATE="*.example.com"表示所有模块路径为example.com的子域名都不进行GOPROXY下载和校验。

3.测试

回归测试,集成测试,单元测试

3.1 单元测试

对一个函数,一个模块输入后,输出与期望的对比

规则如下:

  • 所有测试文件以_test.go结尾
  • 测试函数的命名规范:func TestXxx(*testing.T)
  • 初始化测试放入TestMain
func TestMain(m *testing.M){
    //数据装载、配置初始化等
    code := m.Run()
    //释放资源等

}
func TestHelloTom(t *testing.T){
    output := HelloTom()
    expectOutput := "Tom"
    if output 

使用不等号 image.png

使用开源的assert包 image.png

测试覆盖度 go test judgment_test.go judgment.go --cover 会返回一个代码覆盖率 100%、66.7%。一般达到50%~60%,较高覆盖率80%+(比如资金类的功能,支付)。 测试分支相互独立、全面覆盖。测试单元粒度足够小,函数单一职责。

3.2 Mock测试

含有外部依赖时,使用mock测试 Patch unPatch

3.3 基准测试

分析系统性能时,本地测试

4.项目实践

4.1 需求描述

社区话题页面

  • 展示话题(标题,文字表述) 和回帖列表
  • 实现本地web服务,不用前端
  • 话题和回帖数据用文件存储,不用db

在运行代码时遇到的几个坑:(1)clone老师给的代码以后发现全是找不到包的报错。这是由于vscode只能打开一个项目,也就是说,要打开这个项目的文件夹,保证有且只有一个go.mod。
(2)在gopath文件夹里直接go mod init。达咩。最好要新建src/你的名字/你的项目,进入文件夹,再init。

实体: user, topic, postList ER图: image.png 分层结构:

image.png

  • Repository数据层:数据Model, 封装外部数据(此项目为本地文件)的增删改查,接口只对service层透明。
  • Service逻辑层:业务Service,处理核心业务的逻辑输出。此项目中的Entity就是话题页面。
  • Controller视图层:视图view,处理和外部的交互逻辑。

项目构建过程:

组件工具:

  • Gin 高性能 go web 框架: github.com/gin-gonic/g…
  • Go Mod go mod init :初始化go.mod文件,需要注意的是不能在$GOPATH目录下直接运行(不慎运行可以删除go.mod操作),需要在里面新建文件夹再运行,后面带参数为gopath后的相对路径。

go get gopkg.in/gin-gonic/gin.v1@v1.3.0 :下载文件依赖

从下而上实现,从repository到view

  1. 数据文件,json topic.json
{
    "id":1,
    "title":"青训营来啦!",
    "content":"欢迎大家加入~",
    "create_time":1650437625
}

post.json

{
    "id":1,
    "parent_id":1,
    "content":"欢迎1",
    "create_time":1650437659
}

2.查询功能 QueryTopicById()返回话题 QueryPostsByParentId()返回帖子列表

为了快速查找,不去遍历,而用map定位。

var (
    topicIndexMap map[int64]*Topic
    postIndexMap map[int64][]*Post
)

然后初始化话题数据索引。读文件,将json封装到Topic结构体,存索引。

仿照这个initTopicIndexMap函数实现回帖函数postTopicIndexMap。

3.业务层 创造一个实体——话题页,首先需要进行判空等参数校验,然后准备数据(并行处理话题信息和回帖信息 WaitGroup go func go func),最后组装实体。

type PageInfo struct{
    Topic *repository.Topic
    PostList []*repository.Post
}
  1. Controller层

构建view对象,业务错误码(比如0) type PageData struct{ Code int64 'json:"code"' Msg string 'json:"msg"' Data interface{} 'json:"data"' } func QueryPageInfo(topicIdStr string) *PageData {//这里面调用service层的函数

  1. Router

通过gin搭建web框架:- 初始化数据索引

  • 初始化引擎配置
  • 构建路由
  • 启动服务
  1. 运行 go run server.go

image.png

课后作业:

  • 支持发布帖子
  • 本地Id生成不重复
  • Append文件,更新索引,注意Map的并发安全问题