[ Go依赖管理与工程实践 | 青训营笔记 ]

77 阅读2分钟

[ Go依赖管理与工程实践 | 青训营笔记 ]

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

前言

今日学习了Go语言进阶与依赖管理和Go语言工程实践之测试,写一下我认为的重点。

一.本课重点

并发的深入 1.Goroutine 2.CSP 3.Channel 4.Lock 5.WaitGroup

依赖管理方面 1.GoPATH 2.Go Vendor 3.Go Module

二.详细内容

1.Goroutine

与其他大部分语言提供的协程支持相同,Go 的 Goroutine 是用户态的,其协程栈占用仅有 KB 级别,十分节约系统资源;但不同的是,Goroutine 将协程和并发简化到了仅需一个 go 关键字即可完成,而不像其他语言的协程一样及其繁琐复杂。Goroutine 是有栈协程,而不是如 Kotlin 协程那样的无栈协程。

接下来,让我们看一个典型的 Goroutine 例子,来感受一下 Goroutine 的魅力:

package main
​
import (
    "fmt"
    "time"
)
​
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}
​
func main() {
    go say("world")
    say("hello")
    time.Sleep(time.Second)
}

上文的代码中,定义了一个 say 函数,接受一个字符串形参,作用是每隔 100 毫秒打印一次传入的字符串,重复 5 次;接下来,在 main 函数,调用了两次 say 函数,并传入不同的参数 "hello""world",最后,调用 time.Sleep() 函数令当前协程(也就是 main 函数的主协程)休眠 1 秒。与以往不同的是,第一个 say 函数前被标上了一个 go 关键字 —— 这意味着该函数将在一个新的 Goroutine 协程中运行。

输出为

hello
world
world
hello
hello
world
world
hello
hello

2.WaitGroup

func hello(i int) {
    println("hello goroutine : " + fmt.Sprint(i))
}
​
func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}
​
func main() {
    HelloGoRoutine()
}

一个简单的协程程序,其中 HelloGoRoutine 函数开启了 5 个 go 协程,用于打印输出 1-5 的数字,最后,对主协程进行休眠以确保子协程执行完成。

3.Channel

你是否有想过如何在协程与协程之间进行数据传输和通信?你可能会说,这不是很容易的事吗,直接维护一个数组或者结构体实例之类的,然后不同协程按需取用内部的数据就行啦。但是事实上,这种通过共享内存实现的数据通信是十分危险的,可能会遇到位置的问题。Go 语言建议我们通过其内置的 通道(Channel) 功能来进行数据通信,从而共享内存。

可以通过如下方式声明一个 Channel:

ch := make(chan int)
复制代码

声明了一个名为 ch无缓冲 Channel,并指定 Channel 的传输数据类型为 int

无缓存 Channel 意味着,一个数据的发送必须等待另一端代码的接收,如果没有人接收发送的数据,那么发送端便会被永远阻塞。

可以通过如下方式声明一个带缓冲区的 Channel:

ch := make(chan int, 3)
复制代码

声明一个名为 ch 的有三个缓冲区的 Channel,并指定 Channel 的传输数据类型为 int

这意味着,该 Channel 内可以有最多 3 个数据未被接收方接收,此时,发送方可以直接发送数据而不必收到阻塞,如果超出缓冲区(在本例中为发送第四个数据,且缓冲区被前三个数据占满),则依旧会被阻塞。

可以通过如下方式向 Channel 发送数据:

ch <- v // 假设 v 是一个 int 变量
复制代码

然后,通过如下方式从 Channel 中接受数据:

v := <-ch // 赋值给 v 变量
复制代码

可以通过使用 for range 的方式来从一个 Channel 中取出所有数据:

for i := range ch {
    fmt.Println(i)
}
复制代码

for range 将会始终读取一个 Channel 中发送的数据,直到该 Channel 被关闭。

当一个 Channel 被使用完毕,应当调用 close 函数关闭 Channel:

close(ch)
复制代码

以下代码是一个由三个协程组成的程序:第一个协程负责生成 0-9 的数字并通过 Channel 发送给第二个协程;第二个协程接收收到的数字,并将数字进行平方计算,然后将结果发送给主协程;主协程遍历接收到的结果并输出:

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)
    }
}

4.依赖管理

GOPATH

是一个环境变量,其中有三个部分:

  • bin:项目编译的二进制文件
  • pkg:项目编译的中间产物,加速编译
  • src:项目源码,项目代码直接依赖src下的代码go get下载最新版本的包到src目录下

存在的弊端:无法实现package的多版本控制

Go Vender

项目目录下增加vender文件,所有依赖包副本形式放在$ProjectRoot/vender

依赖寻址方式:vender -> GOPATH

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。

image.png

存在的弊端:更新项目的时候可能导致编译错误的冲突;无法控制依赖的版本。

image.png

Go Module(1.16以后默认开启)

  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包

终极目标:定义版本规则和管理项目依赖关系

三.实践例子

虽然有实践例子,但对于小白我来说有点难以理解与操作。

四.个人总结

第二天的内容比第一天的内容多了好多,自己原先只会写一些小的程序,然后接触这些对于我来说有点困难与难以接受。唉,真的感觉到了抽象。

五.引用参考

  1. 字节内部课:Go 语言入门 - 工程实践
  2. Go 并发 | 菜鸟教程 (runoob.com)