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

156 阅读6分钟

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

概述

本节课程主要涉及go的并发编程,依赖管理,单元/基准/mock测试及项目实践

并发编程

并行与并发

image-20220511113501903.png

线程与协程

image-20220511134027269.png

  • 协程,用户态,轻量级线程
  • 线程,内核态,线程跑多个协程
CSP与Channel

CSP(Communicating Sequential Processes)提倡通过通信共享内存而不是通过共享内存实现通信

image-20220511133854503.png

Channel是golang中一种特殊的类型,主要用于协程间的消息通信,更多示例可参考Go by Example 中文版: 通道

image-20220511133740328.png

image-20220511133710372.png

其他

除了使用channel来实现并发协程之间的信息共享外,我们也可以基于互斥锁机制(sync.Mutex)或sync.WaitGroup来保证多个goroutine 的并发执行

image-20220511133825632.png

image-20220511133817148.png

依赖管理

golang的依赖管理经历了GOPATH到GOVENDOR到现在大家熟知的GOMODULE

image-20220511135033217.png

GOPATH

简介

GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区。 目录有以下结构:src:存放Go项目的源码;pkg:存放编译的中间产物,加快编译速度;bin:存放Go项目编译生成的二进制文件 image-20220511135113446.png

存在问题

image-20220511135257492.png

GOVENDOR

简介

image-20220511135354610.png

存在问题

image-20220511135407764.png

GOMODULE

简介

Go Modules 是Go语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多个版本等问题,go module从Go 1.11 开始实验性引入,Go 1.16 默认开启;

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

Go Module实现了最终的目标:可以定义版本规则和管理项目依赖关系

常用的go mod命令

go mod init
初始化项目
go mod download
用法:go mod download [-dir] [-json] [modules] 使用此命令来下载指定的模块,模块的格式可以根据主模块依赖的形式或者path@version形式指定。如果没有指定参数,此命令会将主模块下的所有依赖下载下来。
​
go mod tidy
默认情况下,go不会移除go.mod文件中的无用依赖。所以当你的依赖中有些使用不到了,可以使用go mod tidy命令来清除它
​
go mod vendor
用法:go mod vendor [-v],此命令会将build阶段需要的所有依赖包放到主模块所在的vendor目录中,并且测试所有主模块的包。同理go mod vendor -v会将添加到vendor中的模块打印到标准输出。
​
go mod verify
用法:go mod verify。此命令会检查当前模块的依赖是否已经存储在本地下载的源代码缓存中,以及检查自从下载下来是否有修改。如果所有的模块都没有修改,那么会打印all modules verified,否则会打印变化的内容。
​
go list -m all
打印当前module的依赖包。也可以添加 -json 参数,例如: go list -m -json all
​
go mod graph
打印模块依赖图

go proxy

关于go module的依赖分发,也就是依赖包从哪里下载以及如何下载的问题。Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。简单来说,go mod可以从远程代码托管系统平台直接下载依赖,比如github等。 但直接使用版本管理仓库下载依赖,也存在多个问题:

  • 无法保证构建确定性:软件作者可以直接代码平台增加/修改/删除软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。
  • 无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用;
  • 大幅增加第三方代码托管平台压力。

image-20220511140924738.png

为解决上述的依赖分发问题,go proxy应运而生。Go Proxy 是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现稳定和可靠的依赖分发;使用 Go Proxy 之后,项目构建时会直接从 Go Proxy 站点拉取依赖。

image-20220511141132810.png

Go Modules通过设置GOPROXY环境变量来控制如何使用 Go Proxy;

image-20220511141107928.png

GOPROXY是一个 Go Proxy 站点URL列表,可以使用“direct”表示源站。对于示例配置,GOPROXY="https://proxy1.cn, https://proxy2.cn, direct"整体的依赖寻址路径 ,会优先从proxy1下载依赖,如果proxy1不存在,后继续去proxy2寻找,如果proxy2中也不存在,则会回源到源站直接下载依赖。

设置命令如下:

window

$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct

linux

$ export GO111MODULE=on
$ export GOPROXY=https://goproxy.cn

单元/基准/mock测试

单元测试和基准测试

在golang中,有着较为完善的测试框架,测试文件一般以_test.go结尾,良好的单元测试编码习惯可以为项目后期的维护迭代带来很大的便利。相关教程可参考Go by Example 中文版: 单元测试和基准测试;具体的测试例子可参考go-project-example/test

测试预处理:

func TestMain(m *testing.M) {
    // 测试前,数据处理,配置初始化等工作
    code := m.Run()
    // 测试后,释放资源等工作
    os.Exit(code)
}

单元测试的代码框架大致如下:

func TestXXX(t *testing.T) {
    var tests = []struct {
        // 表格驱动测试,覆盖多种场景
        a, b int
        want int
    }{
        {0, 1, 0},
        {1, 0, 0},
        {2, -2, -2},
        {0, -1, -1},
        {-1, 0, -1},
    }
    for _, tt := range tests {
        testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
        t.Run(testname, func(t *testing.T) {
            // 主要的测试代码
            ans := IntMin(tt.a, tt.b)
            if ans != tt.want {
                t.Errorf("got %d, want %d", ans, tt.want)
            }
        })
    }
}

基准测试的代码框架大致如下:

func BenchmarkXXX(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // 基准测试主代码
        IntMin(1, 2)
    }
}

在此推荐github.com/stretchr/testify/assert等开源测试库协助测试代码的编写,golang在这一块的开源代码十分丰富

image-20220511122549501.png

mock测试

课程中介绍的monkey库可以通过对函数\方法进行打桩的方式来实现mock效果,十分有趣~

image-20220511122320671.png

image-20220511122455115.png

项目实践

需求描述

社区话题页面

  • 展示话题(标题,文字描述)和回帖列表
  • 暂不考虑前端页面实现,仅仅实现一个本地web服务
  • 话题和回帖数据用文件存储
分层设计

课程中介绍了一种简单的项目分层规范,整体分为三层,repository数据层,service逻辑层,controoler视图层

Repository数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。

Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;

Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。

image-20220511133138394.png

代码实现

具体代码实现可参考学习go-project-example