青训营X豆包MarsCode 技术训练营后端笔记——Go语言工程实践之语言进阶和依赖管理 | 豆包MarsCode AI刷题

31 阅读6分钟

语言进阶

  1. 并发vs并行 image.png 现在我们有一个多线程程序,什么时候叫并发运行,什么时候又叫并行运行呢? 并发:系统只有一个cpu,每个时间段运行一个线程,其他线程处于挂起状态。 并行:系统有多个cpu,则可能是非并发的,多个线程同时在不同cpu上运行。
  2. 协程与线程 image.png 线程是由操作系统管理和调度的,而协程则是由程序代码自身进行管理和调度。线程需要昂贵的开销,而协程开销低,效率高。
  3. 通过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函数。

  1. 协程通信 go语言提倡通过通信共享内存

image.png 通道channel是使得数据从一个goroutine发送到另一个goroutine的通信机制,分为有缓冲make(chan int)和无缓冲通道make(chan int, 2),默认时无缓存的,这意味着在发送时会阻塞,除非有相应的接收操作。 通过通信共享内存不会出现死锁的情况,没有并发安全问题。

  • channel的使用:
    1. channel的声明和创建
    	var ch chan int // 声明一个类型为int的channel此时ch = nil
    	ch = make(chan int, 3)  //初始化/创建
    
    1. channel的操作符
      • 发送:ch<-值
      • 接收:值 := <-ch<-ch
      • 关闭:close(ch)
    2. channel接收和发送的特性
      • 通道关闭后,不可再发送
      • 通道关闭后,仍可继续接收。如果通道已无值,接受得到两个值,第一个是对应通道类型的零值,第二个为false,表示通道已关闭且无值。
      • 若通道未关闭且无值,则接受操作堵塞。
    3. 无缓冲通道必须配对的操作也开始执行,否则阻塞。如下面的程序,子协程想要接受数据,子协程阻塞,等待主协程发送后,子协程收到数据并输出。
    	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("发送成功")
    	}
    
    1. 有缓冲通道 可以通过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)
    	    }
    	}
    
  1. 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()//等待计数器清空
}
  1. 总结
  • 通过goroutine实现高并发
  • 通过channel进行协程通信从而实现内存共享
  • 通过Sync实现协程同步,保证并发安全

依赖管理

依赖管理的演进过程

GOPATH->GO VENDOR ->GO MODULE

  1. GOPATH 其下包含bin(项目编译的二进制文件),pkg(编译中间产物),src(源码)三个文件夹。项目代码直接依赖src中的包代码,go get下载最新版本的包。 存在问题:无法实现package的多版本控制
  2. Go Vendor 项目中新增vendor文件夹,通过依赖寻址,每个项目引入一份依赖的副本,解决了依赖版本冲突问题。 存在问题:本质还是依赖源码。依然对依赖版本概念标识不清,出现依赖版本问题。
  3. Go Module 通过Go.mod文件管理依赖包版本 ,go get/go mod指令工具管理依赖包 依赖管理三要素及其与go module的对应关系:
    • 配置文件,描述依赖 ——go.mod
    • 中心仓库,管理依赖库 ——Proxy
    • 本地工具 ——go get/mod
GO Module
  1. go.mod文件 image.png

    go.mod文件包括以下几个部分:

    • module表示模块声明,定义当前项目的模块路径
    • go:表示go版本,规定原生库版本
    • require表示依赖项声明,用于指明当前模块所依赖的其他模块。每个依赖项由模块路径+模块版本组成。indirect表示间接依赖。
    • replace表示依赖项替换,用于指明所依赖的某个模块的替代模块
    • exclude表示依赖排除,指明需要被排除的特定模块
  2. 依赖分发

  • 解决从哪里下载依赖?如何下载? 从github上下载存在的问题——

image.png 所以要依赖Proxy,实现稳定可靠的依赖分发:Go Proxy是一个服务站点,缓存源站中的软件内容,并且在源站中删除后依然可用。

image.png