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

86 阅读4分钟

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

课程讲授内容

  1. 并发编程,从并发编程角度了解 Go 的高性能;
  2. 依赖管理,了解 Go 的依赖演变路线;
  3. 单元测试,mock 测试和基准测试,提升质量意识;
  4. 项目实战,项目需求、需求拆解和逻辑设计,感受项目的开发落地。

1 语言进阶

1.0 并发 VS 并行

  1. 并发:多线程程序在一个核的 cpu 上运行
  2. 并行:多线程程序在多个核的 cpu 上运行
  3. Go可以充分发挥多核优势,高效运行

1.1 Goroutine

  1. 协程:用户态,比较轻量,栈 KB 级别
  2. 线程:内核态,栈 MB 级别
  3. 线程可以并发得运行多个协程,Go 在高并发设计中体现了巨大优势。

1.2 CSP (Communicating Sequential Processes)

  1. 针对共享的问题:提倡通过通信共享内存,而不是通过共享内存实现通信,因为这会发生内容冲突。

1.png

1.3 channel

  1. 声明 make(chan元素类型,[缓冲大小])
    • 无缓冲通道 make(chan int)
    • 有缓冲通道 make(chan int,2)
  2. 计算平方数案例
    • make 两个 channel,src 传输数字,dest 保存计算后的平方数
     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
        }
     }()
    

注:dest 为有缓冲通道,目的是防止消费者的消费速度影响生产者的平衡效率,因为通常消费者的消费较为复杂,会变得较慢。

1.4 并发安全问题

避免因共享内存而导致的数据错误,利用互斥锁 sync.Mutex

如以下示例中对变量进行 2000 次加 1 操作,5 个协程共同进行。

运用锁的结果达到了我们预想的结果,没有使用锁的结果出现异常。这也说明了在共享内容的读写中要注意可能引起数据的混乱

package main

import (
   "fmt"
   "sync"
   "time"
)

var (
   x    int64
   lock sync.Mutex
)

func addWithLock() {
   for i := 0; i < 2000; i++ {
      lock.Lock()
      x += 1
      lock.Unlock()
   }
}

func addWithoutLock() {
   for i := 0; i < 2000; i++ {
      x += 1
   }
}

func add() {
   for i := 0; i < 5; i++ {
      go addWithLock()
   }
   time.Sleep(time.Second)
   fmt.Println("With Lock:", x)

   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
   time.Sleep(time.Second)
   fmt.Println("Without Lock:", x)
}

1.5 waitgroup

sync.WaitGroup 实现了进程的同步(不会像 Mutex 一样暴力阻塞,优雅)。提供了三个方法:

  1. Add(delta int) 计数器 +delta
  2. Done() 计数器 -1
  3. Wait() 阻塞直到计数器为 0 计数器:开启协程+1;执行结束-1;主协程阻塞直到计数器为0.

2 依赖管理

为的是将重心放在业务逻辑的实现上。

2.1 Go 依赖演进

GOPATH -> Go Vendor -> Go Module

  • 不同环境(项目的)依赖管理
  • 控制依赖库的版本
  1. GOPATH

    • bin 项目编译的二进制文件
    • pkg 项目编译的中间产物,加速编译
    • src 项目源码

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

  1. Go Vendor

    • 项目目录下增加vendor文件,所有依赖包副本形式放在 $ProjectRoot/vendor
    • 依赖寻址方式:vendor =>GOPATH,先到 vendor 下去寻找是否有依赖,没有的话再到 GOPATH 去找。

    通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package依赖的冲突问题

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

  2. Go Module

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

2.3 依赖管理三要素

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

2.4 Go Module 工具

2.4.1 go get

  1. go get xx.org/pkg

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

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

3 单元测试

测试时避免事故的最后一道屏障。

通常有三种测试:回归测试集成测试单元测试

3.1 单元测试

重要:GitHub限流,参考 goproxy.cn/ 配置第三方依赖包。

  • 通过输入到测试单元,得到输出和期望值进行校对,以提升效率和保证质量。
  • 测试单元包括函数、模块等。

3.1.1 单元测试 - 规则

  • 所有测试文件以 _test.go 结尾。
  • 测试函数:func TestXxx(t *testing.T),函数名需要以 Test 开头,其中t参数用于报告测试失败和附加的日志信息。
  • 初始化逻辑放到 TestMain 中。

3.1.2 单元测试 - 运行

  • 命令 go test [flag] [package]go test 命令如果没有参数指定包那么将默认采用当前目录对应的包。
    • 常用参数:-v 用于打印每个测试函数的名字和运行时间;-run 对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被 go test 测试命令运行;
  • assert 包中的 Equal 可判断期望输出和测试结果是否一致。
  • 案例示例
    • 编写待测试函数
    package gotest
    
    func callTom() string {
       return "Tom"
    }
    
    • 编写测试函数
    package gotest
    
    import "testing"
    
    func TestEqualString(t *testing.T) {
       output := callTom()
       expectedOutput := "Tom"
       if output != expectedOutput {
          t.Errorf("Expected %s is not output %s", expectedOutput, output)
       }
    }
    
    • go test 测试结果
    $  go test
    PASS
    ok      github.com/Moonlight-Zhao/go-project-example/myDemo/gotest      0.494s
    

3.1.3 单元测试 - 覆盖率

  • 命令:test git:(V0) x go test judgment_test.go judgment.go --cover

4 项目实战

4.1 需求

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

4.2 话题和帖子结构体

  1. 话题 Topic
    • id
    • title
    • content
    • create_time
  2. 帖子 Post
    • id
    • topic_id
    • content
    • create_time

4.3 分层结构

  • 数据层:数据Model,外部数据的增删改查
  • 逻辑层:业务Entity,处理核心业务逻辑输出
  • 视图层:视图view,处理和外部的交互逻辑

2.png

4.4 组件工具

  1. Gin高性能go web框架:github.com/gin-gonic/g…
  2. Go Mod:go mod init go get gopkg.in/gin-gonic/gin.v1@v1.3.0

4.5 Repository - index

  1. 增加索引,很快定位数据
var (
    topicIndexMap map[int64]*Topic
    postIndexMap map[int64][]*Post
)

4.6 server

  1. 实体
type PageInfo struct {
    Topic    *repository.Topic
    PostList []*repository.Post
}
  1. 流程:参数校验->准备数据->组装实体

4.7 Controller

  1. 构建 View 对象
  2. 业务错误码

4.8 Router

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

课程总结

收获:了解了 Goroutine 实现并发,在共享内存和通信方面知晓了提倡使用通过通信实现内存共享;了解了通过 channel 实现数据共享;还有 mutex(避免因共享内存而导致的数据错误) 和 waitgroup(实现阻塞的优雅方式)。除此之外还有依赖的相关发展和管理以及测试;通过项目实战将前面所学的知识融合,巩固知识,增强编程能力。

存在问题:并发编程和测试方法还需要多实践,项目实战有很多地方还不明白,争取再梳理一遍项目实现过程。