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

103 阅读3分钟

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

并发编程

  • 并发:多线程程序在一个核的CPU上运行,主要通过时间片的切换来实现同时运行的一个状态
  • 并行:多线程程序在多个核的CPU上运行,是实现并发的一个手段

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

  • 协程:用户态,轻量级线程,栈MB级别
  • 线程:内核态,线程跑多个协程,栈KB级别

示例程序

package main
​
import (
   "fmt"
   "time"
)
​
func hello(i int) {
   println("hello goroutine : " + fmt.Sprint(i))
}
func main() {
   for i := 0; i < 5; i++ {
      go func(j int) {
         hello(j)
      }(i)
   }
   time.Sleep(time.Second)
}

CSP(Communicating Sequential Processes)

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

Channel

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

  • 无缓冲通道 make(chan int) 致使两个协程同步化
  • 有缓冲通道 make(chan int 2)

示例程序

子协程发送0-9数字

子协程计算输入数字的平方,主协程输出最后的平方数

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

示例程序

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() {
   x = 0
   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
   time.Sleep(time.Second)
   println("WithoutLock:", x)
   x = 0
   for i := 0; i < 5; i++ {
      go addWithLock()
   }
   time.Sleep(time.Second)
   println("WithLock:", x)
}

WaitGroup

  • Add(delta int) 计数器+delta
  • Done() 计数器-1
  • Wait() 阻塞直到计数器为0

计数器:开启协程+1,执行结束-1,主协程阻塞直到计数器为0

依赖管理

依赖管理演进

GOPATH->Go Vendor->Go Module

  • 不同环境(项目)依赖的版本不同
  • 控制依赖库的版本
  • GOPATH:

    • 环境变量$GOPATH

    • 项目源码直接依赖src下的代码

    • go get下载最新版本的包到src目录下

      弊端

      • 无法实现package的多版本控制
  • Go Vendor:

    • 项目目录下增加vendor文件,所有依赖包副本形式放在$Projectroot/vendor

    • 依赖寻址方式:vendor => GOPATH

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

      弊端

      • 无法控制依赖的版本
      • 更新项目可能出现依赖冲突,导致编译出错
  • Go Module

    • 通过go.mod文件管理依赖包版本
    • 通过go get / go mod 指令工具管理依赖包
    • 终极目标:定义版本规则和管理项目依赖关系

三要素

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

配置文件go.mod

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

version

语义化版本:${MAJOR}.${MINOR}.${PATCH}

v1.3.0,v2.3.0

基于commit伪版本:vx.0.0-yyyymmddhhmmss-abcdefgh123

v1.0.0-20220401081311-c38fb59326b7

indirect:标识间接依赖,即没有导入的依赖

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

当有依赖所依赖的依赖版本冲突时,会选择最低的兼容版本。

中心仓库管理依赖库

回源

  • 无法保证构建稳定性

    增加/修改/删除软件版本

  • 无法保证依赖可用性

    删除软件

  • 增加第三方压力

    代码托管平台负载问题

Proxy

GOPROXY

GOPROXY="proxy1.cn,https://proxy2.cn,…"

服务站点URL列表,"direct"标识源站

proxy1->proxy2->direct

若前面的站点都没有依赖,则回源到第三方代码平台

工具

go get

go get example.orh/okg

后接参数:

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

go mod

go mod

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

测试

  1. 回归测试
  2. 集成测试
  3. 单元测试

从上到下,覆盖率变大,成本降低

单元测试

规则

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

Tips

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

基准测试

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

方法名以Benchmark 开头

项目实战

需求分析

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

需求用例

Topic对Post:一对多

  • Topic

    • id
    • title
    • content
    • create_time
  • Post

    • id
    • topic_id
    • content
    • create_time

\