一、语言进阶
从并发编程的视角了解Go语言高性能的本质。
1、并发 VS 并行
Go语言是为并发而生的,可以充分发挥多核优势,实现高效运行。
(1)并发
多线程程序在一个核的cpu上运行,宏观看是多个线程在同一时间间隔运行,微观看是线程在同一时刻串行运行。
(2)并行
多线程程序在多个核的cpu上运行,实现真正的多个线程同一时刻运行。
2、协程Goroutine
线程在系统态中,它的创建、切换、停止都属于系统操作,因此比较消耗资源。
协程可以理解为轻量级的线程,协程的创建和调度由Go语言本身去完成。
线程可以并发地去跑多个协程!
(1)实际应用
快速打印hello goroutine:0-4
因为要求快速,所以我们要开启多个协程去打印。
Go语言开启协程很简单,只需要在函数前加上go关键字即可。
输出:
3、协程间的通信CSP(Communicating Sequential Processes)
Go语言是通过通信来共享内存,而不是通过共享内存来实现通信!!
4、通道
通过make关键字进行通道创建。
make(chan 元素类型,[缓冲大小])
- 无缓冲通道:make(chan int)
- 有缓冲通道:make(chan int,2)
无缓冲通道也被称为同步通道,可以实现发送的协程和接受的协程同步化。
解决同步问题的方法就是使用有缓冲通道,它不会保证顺序性。
有缓冲通道可以类比于学校的菜鸟驿站货架,货架格子满了以后会引起阻塞,需要我们及时取走快递才能重新将新的快递放至货架上。
For example:
A协程发送0-9的数字;B子协程计算输入数字的平方;主协程输出最后的平方数。
package main
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)
}
}
func main() {
CalSquare()
}
5、并发安全Lock
因为Go语言存在通过共享内存实现通道,因此会出现多种路径同时操作同一块内存的情况。
通过对临界区加锁来保证并发安全。
For example:
对变量执行2000次+1操作,5个协程并发执行。
package main
import (
"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() {
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)
}
func main() {
Add()
}
输出:
6、WaitGroup
WaitGroup也在sync下,可以用来实现并发任务的同步。
它的内部维护了一个计数器,开启协程时+1,执行结束时-1,当为0时表示所有的并发任务已经完成。
For example:
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello goroutine:" + fmt.Sprint(i))
}
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()
}
func main() {
ManyGoWait()
}
输出:
二、依赖管理
1、依赖管理演进
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
(1)GOPATH
是Go语言支持的环境变量,是Go项目的一个工作区。
项目的代码直接依赖src下的代码,可以通过go get下载最新的包到src目录下。
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- 项目源码
弊端:
无法实现package的多版本控制。如图,V2版本的package可能并没有兼容V1版本,这样会导致Project A和ProjectB可能无法构建成功。
(2)Go Vendor
- 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:vendor=>GOPATH,vendor目录下没有才会回到GOPATH目录下寻找。因此通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
弊端:
- 无法控制依赖的版本
- 更新项目后又可能出现依赖冲突,导致编译出错。
同样是依赖源码,并不能很清晰地标识依赖地版本。
(3)Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
2、依赖管理三要素
- 配置文件,描述依赖——go.mod
- 中心仓库管理依赖库——proxy
- 本地工具——go get/go mod
3、依赖配置
(1)go.mod
由三部分组成:模块路径、原生库以及单元依赖。
(2)version版本号
①语义化版本
{MINOR}.${PATCH}
- v :所有版本号都以 v 开头。
- MAJOR 主版本号:意味着有大的版本更新,一般会导致 API 和之前版本不兼容。
- MINOR 次版本号:当做了向下兼容的功能性新增及修改。这里有个不成文的约定需要你注意,偶数为稳定版本,奇数为开发版本。
- PATCH 修订版本号:用户做了向后兼容的 bug 修复。
②基于commit位版本
- 基本版本前缀:通常为 vX.0.0 或 vX.Y.Z-0。vX.Y.Z-0 表明该 commit 快照派生自某一个语义版本,vX.0.0 表明该 commit 快照找不到派生的语义版本;
- 时间戳:格式为“yyyymmddhhmmss”,它是创建 commit 的 UTC 时间;
- 最后是长度为 12 的commit号。
(3)indirect关键字
没有直接依赖的模块会用indirect进行标识。
比如A->B->C,A->B是直接依赖,而A->C是间接依赖。
(4)incompatible关键字
- 主版本在V2以上的模块会在模块路径后增加/vN后缀
- 对于没有go.mod文件且主版本在V2以上的依赖,会加上incompatible
(5)依赖图
如图,若X项目依赖了A1.2、B1.2两个项目,且A1.2、B1.2分别依赖了C项目的v1.3和v1.4两个版本,最终编译时所使用的C项目的版本时v1.4!!
因为每次会选择版本最低的兼容版本,比如现在依赖的C项目有v1.5版本,则选择v1.5版本,以此类推。
(6)依赖分发-回源
依赖分发是指我们的依赖在哪里去下载并且如何去下载。
直接使用版本管理仓库下载以来的话会存在以下问题:
①无法保证构建稳定性
比如我们依赖的仓库作者是可以直接在代码平台进行增加/修改/删除软件版本,这样就可能导致我们无法找到之前的依赖版本
②无法保证依赖可用性
作者可以删除软件
③增加第三方压力
代码托管平台负载问题
(7)依赖分发-proxy
(8)依赖分发-变量GOPROXY
go.mod是通过GOPROXY环境变量来控制proxy的配置。
GOPROXY其实是URL列表,direct是指前面的站点都没有依赖的话会回源到第三方。
GOPROXY="proxy1.cn,http://proxy2.cn,…"
(9)go get工具
(10)go mod工具
三、第二天心得
今天要比昨天时间更足一些,所以学习也能更深入一些。 这一节主要讲了Go语言的依赖管理,一定要牢记依赖管理三要素:go.mod、proxy和go get/go mod工具。