Go语言进阶 | 青训营

58 阅读8分钟

Goroutine

协程:用户态,轻量级线程,栈 KB 级别

线程:内核态,线程跑多个协程,栈 MB 级别

func hello(i int) {  
      println("hello goroutine " + fmt.Sprint(i))
      }
func HelloGoGoRoutine() {
      for i := 0; i < 5; i++ {
         go func(j int) {
             hello(j)                
          }(i)  //调用匿名函数,把i作为参数传入      
      }        
      time.Sleep(time.Second) // 确保协程执行完之前,主协程没有退出}
}

CSP (communicating sequential processes)

提倡通过通信共享内存而不是通过共享内存实现通信

Channel

make(chan 元素类型,[缓冲大小])

  • 无缓冲通道: make(chan int)
  • 有缓冲通道: make(chan int, 2)

并发安全lock

用sync相关关键字,实现并发安全操作和协程间的同步。

WaitGroup

计数器在开启协程的时候加一,执行结束减一,主协程阻塞直到计数器为0

依赖管理

依赖管理演进

GOPATH ---> Go Vendor ---> Go Module

  • 间接依赖最终编译时使用兼容的最低版本

GOPATH

GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区。

目录有以下结构:

  • src:存放Go项目的源码
  • pkg:存放编译的中间产物,加快编译速度
  • bin:存放Go项目编译生成的二进制文件

项目代码直接依赖src下的代码, go get 下载最新版本的包到 src 目录下

弊端:无法实现package的多版本控制

Go Vendor

  • 项目目录下增加 vendor文件,所有依赖以副本形式放入
  • 解决了多个项目用同一个package依赖冲突问题
  • 依赖寻址方式: vendor ---> GOPATH

弊端:无法控制依赖的版本,项目A可能间接依赖同一个package的不同版本,更新容易出现依赖冲突

Go Module

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

依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中央仓库管理依赖库 proxy
  3. 本地工具 go get/mod

依赖配置 go.mod

依赖标识: [Module Path][version]

  • 模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块
  • 如果项目的子包想被单独引用,则需要通过单独的init go.mod文件进行管理。
  • 再下面是依赖的原生sdk版本
  • 最下面是单元依赖,
  • 每个依赖单元用模块路径+版本来唯一标示。

依赖配置-version

gopath和govendor都是源码副本方式依赖,没有版本规则概念,而gomod为了放方便管理则定义了版本规则, 分为语义化版本和基于commit的伪版本。

其中语义化版本包括:[Major].[Major].[Minor].$[PATCH]

  • 不同的MAJOR 版本表示是不兼容的 API,所以即使是同一个库,MAJOR 版本不同也会被认为是不同的模块;
  • MINOR 版本通常是新增函数或功能,向后兼容;
  • 而patch 版本一般是修复 bug ;

而基于commit的为版本包括:vX.0.0-yyyymmddhhmmss-abcdefabcdef1234

  • 基础版本前缀是和语义化版本一样的;
  • 时间戳 (yyyymmddhhmmss), 也就是提交 Commit 的时间
  • 最后是校验码 (abcdefabcdef1234), 包含 12 位的哈希前缀;
  • 每次提交commit后 Go 都会默认生成一个伪版本号。

依赖配置 indirect后缀

标示非直接依赖

依赖配置 incompatible

  • 主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。例子:由于gomodule是1。11实验性引入所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible 后缀
  • 前面讲语义化版本提到,对于同一个库的不同的major版本,需要建立不同的pkg目录,用不同的go mod文件管理,如下面仓库为例,V1版本go mod在主目录下,而对于V2版本,则单独建立了V2目录,用另一个go mod文件管理依赖路径,来表明不同major的不兼容性。,那对于有些V2+tag版本的依赖包并未遵循这一定义规则,就会打上+incompatible 后缀,增加一个compatile的case

工具 -go get

go get example.org/pkg 后面添加:@

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

工具 -go mod

go mod 后面加:

  • init 初始化,创建go.mod文件
  • download 下载模块到本地缓存
  • tidy 增加需要的依赖,删除不需要的依赖

测试

以下从上到下覆盖层逐层变大,成本逐渐降低

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

单元测试

单元测试主要包括,输入,测试单元,输出,以及校对,单元的概念比较广,包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;

一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题

单元测试规则

  • 所有测试文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到 TestMain 中

运行

go test [flags][packages]

覆盖率测试:go test [test.go] [tobetest.go] --cover

  • 一般覆盖率:50%-60%,较高覆盖率80%+
  • 测试分支相互独立、全面覆盖
  • 单元测试粒度够小,函数单一职责

mock

而我们的单测需要保证稳定性和幂等性,稳定是指相互隔离,能在任何时间,任何环境,运行测试。 幂等是指每一次测试运行都应该产生与之前一样的结果。而要实现这一目的就要用到mock机制。

快速mock 函数

  • 为一个函数打桩
  • 为一个方法打桩
  • 这里我们用了Monkey,monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock,反射,指针赋值Mockey Patch 的作用域在 Runtime,在运行时通过通过 Go 的 unsafe 包,能够将内存中函数的地址替换为运行时函数的地址。,将待打桩函数或方法的实现跳转到。感觉像是直接跳过部分函数固定其返回好测试后面的是可以说的吗,不过很适合调试代码

基准测试

Go 语言还提供了基准测试框架,基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试,能够

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供了基准测试的能力
运行和优化

讲的东西还不是很懂,先不记哩

项目实践

分层结构

File ---> repository ---> service ---> controller ---> client

model entity view

数据层 逻辑层 视图层

  • 数据层:数据model,外部数据的增删改查数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。
  • 逻辑层:业务entity,处理核心业务逻辑输出。计算打包业务实体entiy,对应我们的需求,上送给视图层
  • 视图层:视图view,处理和外部的交互逻辑,对于我们需求,我们封装json格式化的请求结果,api形式访问

组件工具

  • Gin 高性能 go web 框架
  • Go mod

实战

数据层

一方面查询我们可以用全扫描遍历的方式,但是这虽然能达到我们的目的,但是并非高效的方式,所以这里引出索引的概念,索引就像书的目录,可以引导我们快速查找定位我们需要的结果;这里我们用map实现内存索引,在服务对外暴露前,利用文件元数据初始化全局内存索引,这样就可以实现0(1)的时间复杂度查找操作。

分号主要是一行内语句的分割

直接根据查询key获得map中的value就好了,这里用到了sync.once,主要适用高并发的场景下只执行一次的场景,这里的基于once的实现模式就是我们平常说的单例模式,减少存储的浪费。 有了topic的查询代码,大家可以照猫画虎 自行实现一下根据话题id查询回帖列表的查询方法

service层/逻辑层

参数校验---> 准备数据---> 组装实体

感觉这里像pojo里面非data层()

但是又有service的功能

在后期做项目开发中,一定要思考流程是否可以并,通过压榨CPU,降低接口耗时,不要一味的串行实现,浪费多核cpu的资源

controller层

返回的错误码在这里写结构体

router层

  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务

是web服务的引擎配置,包括 ***** ,path映射到具体的controller。通过path变量传递话题id