语言进阶
- 并发vs并行
现在我们有一个多线程程序,什么时候叫并发运行,什么时候又叫并行运行呢? 并发:系统只有一个cpu,每个时间段运行一个线程,其他线程处于挂起状态。 并行:系统有多个cpu,则可能是非并发的,多个线程同时在不同cpu上运行。
- 协程与线程
线程是由操作系统管理和调度的,而协程则是由程序代码自身进行管理和调度。线程需要昂贵的开销,而协程开销低,效率高。
- 通过
helloRoutine感受 使用go关键字来创建十个goroutine协程,从而实现快速打印。最后的time.sleep的作用是用暴力的方式保证在子协程执行结束之前主协程不退出,如果把这一行去掉,就不能打印出结果了。
func hello(i int) {
fmt.Println("hello goroutine ", i)
}
func helloRoutine() {// 开启10个goroutine
for i := 0; i < 10; i++ {
go hello(i)
}
time.Sleep(time.Second)
}
运行结果
PS C:\Users\86136> go run "c:\Users\86136\Documents\go\goroutine.go"
hello goroutine 0
hello goroutine 3
hello goroutine 9
hello goroutine 4
hello goroutine 5
hello goroutine 6
hello goroutine 7
hello goroutine 8
hello goroutine 1
hello goroutine 2
我依然有疑问:这些协程的创建顺序是从0到10的,为什么会同时运行?
AI回答:在Go语言中,创建协程(goroutine)和它们开始执行是两个不同的步骤。当你使用go关键字创建一个新的协程时,实际上你是在告诉Go运行时“稍后执行这个函数”,而不是“立即执行”。这意味着,所有的协程都是在helloRoutine函数的循环中被创建的,但它们的执行并不是在循环中同步进行的,而是被异步调度。
异步执行:每个go hello(i)调用都会创建一个新的协程(子协程),并将hello函数的执行安排在未来的某个时间点。这些协程的执行不会阻塞helloRoutine函数(主协程)的继续执行,也不会等待彼此。这也就解释了为什么还需要加一个sleep函数。
- 协程通信 go语言提倡通过通信共享内存
通道
channel是使得数据从一个goroutine发送到另一个goroutine的通信机制,分为有缓冲make(chan int)和无缓冲通道make(chan int, 2),默认时无缓存的,这意味着在发送时会阻塞,除非有相应的接收操作。
通过通信共享内存不会出现死锁的情况,没有并发安全问题。
- channel的使用:
- channel的声明和创建
var ch chan int // 声明一个类型为int的channel此时ch = nil ch = make(chan int, 3) //初始化/创建- channel的操作符
- 发送:
ch<-值 - 接收:
值 := <-ch或<-ch - 关闭:
close(ch)
- 发送:
- channel接收和发送的特性
- 通道关闭后,不可再发送
- 通道关闭后,仍可继续接收。如果通道已无值,接受得到两个值,第一个是对应通道类型的零值,第二个为false,表示通道已关闭且无值。
- 若通道未关闭且无值,则接受操作堵塞。
- 无缓冲通道必须配对的操作也开始执行,否则阻塞。如下面的程序,子协程想要接受数据,子协程阻塞,等待主协程发送后,子协程收到数据并输出。
func main() { ch := make(chan int) //创建一个int类型的无缓冲通道 go func(c chan int) { //创建一个匿名函数 fmt.Println("匿名函数执行") c<-5 // 子goroutine从通道中接收数据 fmt.Println("接收成功", ret) }(ch) res := <-ch //在main主goroutine中向ch通道发送数据 fmt.Println("发送成功") }- 有缓冲通道
可以通过
len(ch)获取元素数量,cap(ch)获取通道容量。相比无缓冲通道的同步传输,有缓冲通道是异步方式传递数据的。
func calSquare(){ src := make(chan int) // 构建一个无缓冲的通道 dest := make(chan int,3) // 构建一个有缓冲的通道 go func() { //构建一个goroutine,向src通道发送数字1到10 defer close(src) for i:=0;i<10;i++{ src <- i } }() go func(){// 构建一个goroutine,从src通道接收数字,计算平方后发送到dest通道 defer close(dest) for i:= range src{ fmt.Println("len of src: ",len(src)) dest <- i*i } }() // 从dest通道接收数字并打印 for i:= range dest{ fmt.Println("len of dest: ",len(dest)) fmt.Println(i) } }
- WaitGroup——Sync
主协程阻塞直到计数器为0。使用了
sync.WaitGroup来等待一组 goroutine 的完成,通过waitgroup优化3中的代码:
func ManyGoWait(){
var wg sync.WaitGroup //声明一个WaitGroup类型的变量
wg.Add(10) //向计数器增加十个计数
for i:=0;i<10;i++{
go func(j int) {//创建子协程
defer wg.Done()//子协程结束时调用wg.Done,减少计数
hello(j)
}(i)
}
wg.Wait()//等待计数器清空
}
- 总结
- 通过goroutine实现高并发
- 通过channel进行协程通信从而实现内存共享
- 通过Sync实现协程同步,保证并发安全
依赖管理
依赖管理的演进过程
GOPATH->GO VENDOR ->GO MODULE
- GOPATH 其下包含bin(项目编译的二进制文件),pkg(编译中间产物),src(源码)三个文件夹。项目代码直接依赖src中的包代码,go get下载最新版本的包。 存在问题:无法实现package的多版本控制
- Go Vendor 项目中新增vendor文件夹,通过依赖寻址,每个项目引入一份依赖的副本,解决了依赖版本冲突问题。 存在问题:本质还是依赖源码。依然对依赖版本概念标识不清,出现依赖版本问题。
- Go Module
通过Go.mod文件管理依赖包版本 ,
go get/go mod指令工具管理依赖包 依赖管理三要素及其与go module的对应关系:- 配置文件,描述依赖 ——go.mod
- 中心仓库,管理依赖库 ——Proxy
- 本地工具 ——go get/mod
GO Module
-
go.mod文件
go.mod文件包括以下几个部分:
- module表示模块声明,定义当前项目的模块路径
- go:表示go版本,规定原生库版本
- require表示依赖项声明,用于指明当前模块所依赖的其他模块。每个依赖项由模块路径+模块版本组成。indirect表示间接依赖。
- replace表示依赖项替换,用于指明所依赖的某个模块的替代模块
- exclude表示依赖排除,指明需要被排除的特定模块
-
依赖分发
- 解决从哪里下载依赖?如何下载? 从github上下载存在的问题——
所以要依赖Proxy,实现稳定可靠的依赖分发:Go Proxy是一个服务站点,缓存源站中的软件内容,并且在源站中删除后依然可用。