进阶知识
go并发
并发
并发分为单核并发和多核并发
单核并发是通过时间片轮转来实现的,多核并发则相对简单一些
多线程程序在单核CPU上运行
多线程程序在多核cpu上运行
go语言的机制,可以使go充分发挥多核优势,高效运行
Goroutine(协程)
协程是比线程级别小的状态,就像进程上可以有多个线程,线程上可以有多个协程,就像c/c++在多线程上实现高并发,而go语言则在多协程下实现高并发
协程:用户态,轻量级线程,栈KB级别
线程:内核态,线程跑多个协程,栈MB级别
go语言是通过协程来实现高并发,由于协程的这种机制,也使得go语言更适合高并发编程
创建协程
go语言创建协程格式
go 协程函数
示例代码
package main
import (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
func hello(i int) {
fmt.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)
}
运行结果
从运行结果中体现了并发现,也可以看出高并发协程执行顺序的不确定性
协程间通信
有了多协程,那一定就会有协程间的通信机制
go提倡通过通信共享内存而不是通过共享内存而实现通信,也是为了避免多协程下访问临界资源而带来不必要的麻烦
Channel(通道)
go的通道通信与线程间的管道通信还是比较相似的
创建通道
make(chan 元素类型,[缓冲大小])
无缓冲通道 make(char int)
有缓冲通道 make(char int,2)
示例代码
创建两个协程,其中A子协程发送0~9数字、B子协程计算输入数字的平方,主协程输出最后的平方数
package main
import (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
func main() {
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 {
fmt.Println(i)
}
}
运行结果
从运行结果可以看出,通道满足先进先出
锁机制
在多协程中,对非线程安全的临界资源访问,可能会出现非预期的结果,所以就需要锁机制来实现同步或互斥操作
示例代码
package main
import (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
)
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithnotLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithnotLock()
}
time.Sleep(time.Second)
fmt.Println("WithnotLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
fmt.Println("WithLock:", x)
}
运行结果
WaitGroup(P/V操作、计数器)
go语言中也有pv操作,用来等待子协程结束或进行同步或互斥操作
WaitGroup提供了三个方法用来对计数器进行操作
Add(delta int) // 初始化计数器 计数器 + delta
Done() // 计数器-1
Wait() // 阻塞直到计数器为0
示例代码
将go并发的示例代码进行修改
package main
import (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup // 声明计数器变量
wg.Add(5) // 计数器初始化
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done() // 计数器-1
hello(j)
}(i)
}
wg.Wait() // 阻塞等待计数器值为0
}
运行结果
依赖管理
背景
在项目开发中,不可能是从0开始一行行的代码敲,都是基于已经开发好的代码块进行编程,这样也能大大增加开发效率,这也是模块化编程的理念
工程项目不可能基于标准库进行编码
管理依赖库
Go依赖管理演进
不同环境(项目)依赖的版本不同
控制依赖库的版本
GOPATH
GOPATH为go语言的环境变量,对程序依赖,和包下载提供路径
项目代码直接依赖src下的代码
go get下载最新版本的包到src目录下
GOPATH-弊端
场景:A和B依赖于某一package的不同版本
问题:无法实现package的多版本控制
Go Vendor
项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
依赖寻址方式:vendor => GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
Go Vendor-弊端
问题:
无法控制依赖的版本
更新项目又可能出现依赖冲突,导致编译出错
Go Module
通过go.mod文件管理依赖包版本
通过go get/go mod指令工具管理依赖包
最终目标:定义版本规则和管理项目依赖关系
依赖管理三要素
1. 设备文件,描述依赖 go.mod
2. 中心仓库管理依赖库 Proxy
1. 本地工具 go get/mod
依赖配置-go.mod
依赖标识:[Module Path] [Version/Pseudo-version]
依赖配置-version
语义化版本
{MINOR}.${PATCH}
v1.3.0
v2.3.0
基于commit伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh1234
v0.0.0-20220401081311-c35fg59326f7
v1.0.0-20201130134442-10cb98267c6c
依赖配置-indirect
A→B→C A→B 直接依赖 A→C间接依赖
依赖配置-incompatible
主版本2+模块会在模块路径添加/vN后缀
对于没有go.mod文件并且主版本2+的依赖,会+incompatible
依赖配置-依赖图
如果一个程序,对一个模块有多个版本依赖,则选择最低可以支持的版本进行依赖
依赖分发-回源
1.无法保证构建稳定性
增加/修改/删除软件版本
2.无法保证依赖可用性
删除软件
3.增加第三方压力
代码托管平台负载问题
依赖分发-Proxy
采用中间件的方式,来保证程序依赖的稳定性,在Proxy中会缓存和管理Developer所需要的依赖
依赖分发-变量GOPROXY
GOPROXY="proxy1.cn,https://proxy2.cn,…"
服务站点URL列表,"direct"表示源站