Go 语言进阶与依赖管理 | 青训营笔记

100 阅读5分钟

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

01 本堂课重点内容

  • 并发编程
    • Goroutine
    • CSP(Communicating Sequential Processes)
    • Channel
    • 并发安全Lock
    • WaitGroup
  • 依赖管理
    • GOPATH、Go Module
    • 依赖管理三要素(类比Maven)
      • go.mod —— 配置文件,描述依赖
      • Proxy —— 中心仓库管理依赖库
      • go mod —— 本地工具

02 详细知识点介绍

2.1 并发编程

2.1.1 Go运行快的原因

  • Go 可以充分发挥多核优势,高效运行
  • 并发 VS 并行
    • 并发:多线程程序在一个核的CPU上运行
    • 并行:多线程程序在多个核的CPU傻瓜运行
  • 进程、线程、协程
    • 进程是操作系统进行资源分配的基本单位,每个进程都有自己的独立内存空间
      • 由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全
    • 线程又叫做轻量级进程,是进程的一个实体,是处理器任务调度和执行的基本单位。
      • 线程只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
      • MB级别
    • 协程是一种比线程更加轻量级的一种函数
      • 协程既不是进程也不是线程,协程仅是一个特殊的函数。协程、进程和线程不是一个维度的
      • 创建和调度由Go语言本身去完成
      • KB级别
        • Go语言一次可以创建上万的协程

uTools_1673874408032.png

  • Goroutine(协程)
    • 在函数之前加上go关键字就可以创建
    • time.Sleep是为了保证子协程没完成之前主线程不退出
    • 乱序

image.png

2.1.2 协程之间通信

  • Go提倡通过通信共享内存
    • 这和 Java 语言不通,Java 中多个线程传递数据的方式一般都是通过共享内存或者其他共享资源的方式解决线程竞争问题
    • Channel通道把协程做了一个连接,如同传输队列,遵循先入先出,能保证数据的顺序
    • Go也保留通过共享内存实现数据交换(临界区、互斥量加锁)——会影响程序性能

image.png

2.1.3 Channel

  • make (chan 元素类型, [缓冲大小])
    • 无缓冲通道——同步通道
    • 有缓冲通道——典型的生产者与消费者模型
ch0 := make(chan int) // 创建 无缓冲channel
ch := make(chan int, 10) // 创建 有缓冲channel
ch <- 1 // 写入
v := <- ch // 读取

image.png

  • src channel:实现A、B协程通信
  • dest channel:消费者是M协程,消费者的处理逻辑更加复杂一点,速度可能略慢
  • 保证顺序、并发安全
  • defer:延迟的资源关闭

image.png

2.1.4 并发安全Lock

  • 通过共享内存实现数据交换
  • 不加锁可能会输出未知的结果
  • 加锁性能会更差
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() {
	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) //
}

2.1.5 WaitGroup

  • 不优雅的阻塞:time.Sleep(time.Second)
    • 不清楚子协程的具体执行时间
  • WaitGroup
    • Add(delta int)——计数器 + delta
    • Done()——计数器 - 1
    • Wait()——阻塞直到计数器为0
    • 计数器
      • 开启协程 + 1
      • 执行结束 - 1
      • 主协程阻塞知道计数器为0

image.png

2.2 依赖管理

2.2.1 Go依赖管理演进

  • GOPATH —> Go Vendor —> Go Module
  • 不同项目依赖的版本不同
    • 需要控制依赖库的版本
  • GOPATH
    • go get在1.17版本已经被丢弃
    • 无法实现package的多版本控制

image.png

image.png

  • Go Vendor
    • 基于GOPATH多了一个副本
    • 优先从Vendor获取,Vendor没有再去GOPATH寻找
    • 依赖于项目源码,并不能很清晰的标识版本

image.png

image.png

  • Go Module
    • 定义版本规则和管理项目依赖关系
    • 1.16默认开启
    • 通过go.mod文件管理依赖包版本
    • 通过go mod指令管理依赖包

2.2.2 依赖配置

  • go.mod
    • 资源路径
    • 原生库
    • 单元依赖 = 模块PATH + 版本号

image.png

  • version
    • 语义化版本
      • MAJOR.{MAJOR}.{MINOR}.${PATCH}
        • MAJOR:大版本,可以是不兼容的(代码隔离的)
        • MINOR:新增函数或功能,向后兼容
        • PATCH:一般是修复 bug
    • 基于 commit 的伪版本
      • vx.0.0-yyyymmddhhmmss-abcdefgh1234
        • 版本前缀
        • 时间戳
        • 哈希校验码前缀
  • indirect
    • A —> B —> C
      • A —> B 直接依赖
      • A —> C 间接依赖
  • incompatible
    • 主版本2+模块会在模块路径增加/vN的后缀
      • 可以不同主版本不相互兼容
    • 对于没有go.mod文件并且主版本2+的依赖,会+incompatible
  • 依赖图

image.png

2.2.3 依赖分发

  • 依赖分发
    • 解决依赖去哪里下载、如何下载的问题
    • 直接使用版本管理仓库下载依赖,存在多个问题
      • 首先无法保证构建确定性:软件作者可以直接代码平台增加/修改/删除 软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本
      • 无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用
      • 大幅增加第三方代码托管平台压力
    • go proxy就是解决这些问题的方案
      • go proxy是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供“immutability”和“available”的依赖分发
      • 使用go proxy之后,构建时会直接从go proxy站点拉取依赖

image.png

image.png

  • 变量 GOPROXY
    • GOPROXY是一个Go Proxy站点URL列表,使用“direct”表示源站。
    • 对于示例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下钻proxy2寻找,如果proxy2,中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。

image.png

2.2.4 工具

  • 做项目之前都可以执行一下go mod tidy命令

image.png

03 引用参考 & 文章收藏