后端day2-工程进阶 | 青训营笔记

54 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 2 天

一、本堂课重点内容

本节课程主要分为四个方面:

  1. 并发编程:包括协程、通道、锁、线程同步
  2. 依赖管理:Gopath, Go Vendor, Go Module
  3. 单元测试
  4. 项目实战

二、详细知识点介绍

1. 并发编程
  • Goroutine

    • 协程与线程

      • 线程:内核态、一个线程有多个协程、在栈上为mb级别。
      • 协程:用户态、轻量级线程、在栈上为kb级别。
    • 开启一个协程

      • 使用匿名函数创建goroutine

        go func( 参数列表 ){
            函数体
        }( 调用参数列表 )
        
      • 为一个普通函数创建goroutine

        go 函数名( 参数列表 )
        
    • CSP (Communicating Sequential Processes)并发模型:提倡通过通信共享内存而不是通过共享内存实现通信。

      image.png
  • Channel

    • 定义一个通道:make(chan 元素类型,[缓冲大小])

    • 可以定义无缓冲通道,可以保证发送和接收协程的同步;定义带缓冲通道,可以解决消费者消费速度比生产者的生产速度慢的问题。

    • eg. channel的应用:通过sec和dest的传递保证了输出的顺序性

      func CalSquare() {
      	src := make(chan int)
      	dest := make(chan int, 3)
      	go func() {
      		defer close(src)
      		for i := 0; i < 10; i++ {
      			src <- i
      		}
      	}()
      	go func() {
      		defer close(dest)
      		for i := range src {
      			dest <- i * i
      		}
      	}()
      	for i := range dest {
      		//可能存在的复杂操作
      		println(i)
      	}
      }
      
  • Lock:保证并发安全

    • 锁的定义:var lock sync.Mutex
    • 加锁和解锁:lock.Lock()/lock.Unlock()
  • WaitGroup:实现并发任务的同步

    image.png
    • WaitGroup的定义:var wg sync.WaitGroup

    • eg. 等待五个协程

      func ManyGoWait() {
      	var wg sync.WaitGroup
      	wg.Add(5)
      	for i := 0; i < 5; i++ {
      		go func(j int) {
      			defer wg.Done()
      			hello(j)
      		}(i)
      	}
      	wg.Wait()
      }
      
2. 依赖管理
  • 依赖管理演进:GOPATH -> Go Vender ->Go Module

    • GOPATH

      • GOPATH是go语言的环境变量,它是go项目的工作区。

      • 目录下内容:

        image.png
      • 项目代码直接依赖src下代码。

      • 使用go get下载最新版本的包在src目录下。

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

    • Go Vender

      • 项目目录下增加vendor文件,所有依赖包的副本放在$ProjectRoot/vendor目录下。
      • 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
      • 弊端:无法控制依赖的版本、更新项目可能出现依赖冲突。
    • Go Module

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

    • 配置文件,描述依赖:go.mod

    • 中心仓库管理依赖库:Proxy

    • 本地工具:go get/mod

  • 依赖配置

    • go.mod

      image.png

      依赖标识:[Module Path] [Version/Pseudo-version]

    • version

      • 语义化版本:${MAJOR}.${MINOR}.${PATCH}
        • MAJOR:大版本,互相可以不兼容
        • MINOR:新增功能,版本间一般兼容
        • PATCH:代码bug修复
      • 基于commit伪版本:vX.0.0-时间戳-12位hash码
    • indirect

      • 对于没有直接导入的间接依赖模块,用indirect关键字标识。
    • incompatible

      • 对于没有go.mod文件以及主版本在v2及以上的依赖,加上incompatible后缀,标识可能有兼容问题。
    • 依赖图:选择满足本次构建的最低的兼容版本

      image.png
    • 依赖分发

      • 直接从github中下载依赖存在一些问题:无法保证构建确定性、无法保证依赖可用性、增加了第三方代码托管平台的压力。

      • go proxy作为上述问题的解决方案。它会缓存源站中的软件内容,缓存的软件版本不会改变,并且源站删除之后仍然可用,构建时会直接从proxy中拉取依赖。

        image.png
      • 环境变量GOPROXY配置:它是一个url列表,可以使用direct表示源站。

        • eg.GOPROXY=‘’proxy1.cn, proxy2.cn, direct”,寻找依赖时会优先从proxy1下载依赖,若不存在再从proxy2寻找,否则就会回到源站下载依赖再缓存到proxy站点中。
    • go get/mod的使用

      image.png
3. 测试
  • 测试的分类:回归测试、集成测试、单元测试。

    • 测试成本逐渐降低,覆盖率逐步上升。
    • 单元测试的覆盖率决定着代码质量。
  • 单元测试

    • 规则

      image.png
    • 使用assert函数来判断测试的正确性

      image.png
    • 覆盖率:衡量代码是否已经经过了足够的测试

      • 一般来说要达到50%~60%,要求严格的资金型服务需要80%以上。
      • 测试单元粒度小可以提升覆盖率。
  • mock测试

    • 工程中复杂的项目一般会依赖数据库和文件,单元测试需要保证稳定性幂等性,要实现这一目的需要mock机制。

    • 开源的mock测试库:monkey

    • 通过patch对函数进行打桩,摆脱对于文件的束缚和依赖。

      image.png
  • 基准测试

    • 规则

      • 所有测试文件以_test.go结尾。
      • 测试函数为func BenchmarkXxx(b *testing.B)
    • b.ResetTimer()函数重置计时器。

    • 多协程并发测试示例:

      func BenchmarkFastSelectParallel(b *testing.B) {
      	InitServerIndex()
      	b.ResetTimer()
      	b.RunParallel(func(pb *testing.PB) {
      		for pb.Next() {
      			FastSelect()
      		}
      	})
      }
      
      • rand.Intn()函数为了并发安全,使用了全局锁,导致并发情况下的速度劣化。
      • 高性能随机数方法:fastrand.Intn(),牺牲一定的数列一致性,提高了效率。

三、实践练习例子

  1. 需求描述:实现社区话题页面,展示话题和回帖列表。不考虑前端实现,仅实现本地web服务。

  2. E-R图设计:实体包括话题和贴子,关系为一对多,设计出实体的属性。

    image.png
  3. 分层结构:数据层->逻辑层->视图层

    image.png
    • 数据层屏蔽了下游的数据差异,对逻辑层的接口模型不变。
  4. 基础组件和工具

    • gin:go web框架
    • go mod:实现依赖管理
  5. 具体实现

    • repository
      • 利用索引实现查询(map)。
      • 利用结构体封装查询操作,业务逻辑层只需要调用结构体的方法即可完成相应的操作。
      • sync.Once类型的.Do操作保证操作的函数在多线程环境下只会被执行一次,这里用来保证程序中只有一个查询结构体的实例存在。
    • service
      • 参数校验:检查传入的参数是否合法。
      • 准备数据:调用repository层提供的接口进行查询,注意其中可以并行执行的流程,降低接口耗时。
      • 组装实体:将查询到的数据组装起来并返回。
    • controller:调用service层的接口构建view对象,并返回业务错误码。
      • 空接口:interface{},可以表示任意类型的变量。
      • PageData结构体的内容为错误码、错误信息、返回的数据。
    • router:通过gin搭建web框架,流程为
      • 初始化数据索引:在本例中为构建map索引
      • 初始化引擎配置:r := gin.Default()
      • 构建路由:使用r的方法。
      • 启动服务:服务通过http暴露出去。
  6. 问题总结

    • 完善环境变量

      • go env -w GOPROXY="https://goproxy.io"
      • go env -w GO111MODULE="on"
      • 可以在创建项目时在环境中填写:https://goproxy.cn,direct
    • 配置gin

      • 建立mod:go mod init 文件夹名
      • 下载Gin环境依赖(用 -u 标记来更新本地的对应的代码包):go get -u github.com/gin-gonic/gin
      • 继续完善依赖,下载到本地:go mod download
      • 增加缺少的module,删除无用的module:go mod tidy
      • import "github.com/gin-gonic/gin"
    • 运行并测试

      • 启动服务:go run server.go
      • 测试:curl –-location –-request GET http://127.0.0.1:8080/community/page/get/2 | python -m json.tool