零基础学习Go的Day09| 青训营笔记

68 阅读3分钟

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

1.复习已学知识

  • 并发和并行的区别
  • 学习并发的概念
  • 使用Goroutine

2.观看Go语言进阶与依赖管理

  • 学习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 的数字,最后,对主协程进行休眠以确保子协程执行完成。

如果您对这段代码有所疑惑,那么对于:

func(j int) {
  hello(j)
}

这部分代码,其实是 Go 的匿名函数语法,他创建了一个未定义名称的函数,声明了一个 int 类型的形参 j,并在该函数的函数体内调用了 hello 函数。接下来在其后面添加()并传入实参 i 以直接调用这个匿名函数。

为了保证子协程执行完毕后主协程才会退出,改造 HelloGoRoutine 函数:

func HelloGoRoutine() {
    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()
}

time.Sleep(time.Second) 被移除,取而代之的是 wg.Wait(),除此之外,函数还增加了许多其他调用,它们都是什么意思?让我们一步一步解析:

  1. 首先,var wg sync.WaitGroup 声明了一个名为 wgWaitGroup 变量;
  2. 紧接着,调用 WaitGroup 实例的 Add 方法,传入数字 5
  3. 在匿名函数开头,延迟调用 WaitGroup 实例的 Done 方法;
  4. 最后,在函数尾部调用 WaitGroup 实例的 Wait 方法。

如果您还不了解 defer 关键字,其实该关键字代表“在函数末尾执行”,被 defer 关键字标注的代码会以 先进后出 的顺序被移动到函数末尾执行,这就意味着:

defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")

实际上等价于:

fmt.Println("3")
fmt.Println("2")
fmt.Println("1")

defer 关键字常用于资源释放等用处:比如,我们打开了一个 IO 流,需要在函数结束时释放流(调用 Close 方法),这时,我们便可以直接在打开的流实例下直接填写:

defer stream.Close()

来确保流一定会在函数结束时释放,避免遗忘。

回到开头,WaitGroup 其实内部维护了一个计数器,并以如下方式工作:

  • 通过调用 Add 方法,向 WaitGroup 的计数器添加指定值;
  • 通过调用 Wait 方法阻塞当前协程,这会使得协程陷入无限的等待;
  • 通过调用 Done 方法使 WaitGroup 内部的计数器 -1,直到计数器值为 0 时,先前被阻塞的协程便会被释放,继续执行接下来的代码或是直接结束运行。

因此,上述代码为 WaitGroup 的计数器 +5,随后阻塞主协程,当所有 5 个子协程纷纷调用 Done 方法后,主协程便会被释放,然后结束程序运行。怎么样,是不是很优雅?