Go 语言进阶 | 青训营笔记

78 阅读3分钟

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

线程与协程

  • 线程:内核态,线程可以跑多个协程,栈 MB 级别(1-8MB)
  • 协程:用户态,轻量级线程,栈 KB 级别(2KB)

GO 中,可以直接在函数体调用前面加入关键字 go,即可为其创建一个协程( goroutine )来运行

值得注意的是:

  1. 当主函数运行完,但仍有协程在运行时,程序也会直接结束
  2. 因为不同协程在同一段时间内是并发/并行,因此他的运行顺序不一定是按照创建协程的顺序进行

协程间通信

Go语言提倡 通过通信共享内存 而不是 通过共享内存实现通信

协程可以通过 channel 来通信

channel 可以通过管道来创建 make(chan type, num)

  • type 是数据类型
  • num 是管道的缓冲长度,如果不填写该参数,则无缓冲长度

不同协程在访问相同内存的时候会存在问题,因为协程是并发/并行的,当多个读写操作同时进行时(至少有一个是写操作),赋值就会出现问题

进一步解释:假设此时有 A1 和 A2 两个协程,如果都对相同内存 cnt 进行赋值 + 1 操作,可能出现如下情况:

cnt = 0
A1 读取 cnt = 0
A2 读取 cnt = 0
A1 写入 cnt = 1
A2 写入 cnt = 1

如下代码中,运行后发现答案不一定为 100000

package main

import (
    "fmt"
    "time"
)

var cnt int = 0

func calc(n int) {
    for i := 0; i < n; i++ {
        cnt += 1
    }
}

func main() {
    for i := 0; i < 100; i++ {
        go calc(1000)
    }
    time.Sleep(2 * time.Second)
    fmt.Println(cnt)
}

为了解决上述问题,引入了锁的概念,保证在任意一个时刻,处理相同内存的至多只有一个协程

var mx sync.Mutex
func calc(n int) {
    mx.Lock()
    defer mx.Unlock()
    for i := 0; i < n; i++ {
        cnt += 1
    }
}

在上述内容中,曾提及“当主函数运行完,但仍有协程在运行时,程序也会直接结束”,因此考虑三种方法来解决该问题

  1. 如上述代码一般,直接让主函数挂起一定时间,保证其他 goroutine 能够运行完
  2. 使用 channel 返回信号,这里适用于知道有多少个 goroutine
package main

import (
    "fmt"
    "sync"
)

var cnt int = 0
var mx sync.Mutex

func calc(c chan int) {
    mx.Lock()
    for i := 0; i < 100; i++ {
        cnt += 1
    }
    mx.Unlock()
    c <- 1
}

func main() {
    c := make(chan int)
    for i := 0; i < 1000; i++ {
        go calc(c)
    }
    for i := 0; i < 1000; i++ {
        <-c
    }
    fmt.Println(cnt)
}
  1. 使用 sync.WaitGroup

当进入一个 goroutine 的时候,进行 Add(1),当其退出的时候,运行 Done();在主函数用 Wait() 等待

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

var cnt int = 0
var mx sync.Mutex
var wg sync.WaitGroup

func calc() {
    wg.Add(1)
    defer wg.Done()
    mx.Lock()
    for i := 0; i < 100; i++ {
        cnt += 1
    }
    mx.Unlock()
}

func main() {
    rand.Seed(time.Now().Unix())
    for i := 0; i < rand.Intn(1000); i++ {
        go calc()
    }
    wg.Wait()
    fmt.Println(cnt)
}

依赖管理

go modelGo 最新的用来控制版本的方案,主要是处理项目中引入了多个不同版本的相同库,则需要通过 go model 来进行管理

下面是简单的使用命令总结:

初始化:go mod init [address],使用后会出现一个 go.mod 文件

下载库:go get -u url,使用后会将 url 对应的库包下载到 GOPATH/pkg/mod 里面,并且产生一个 go.sum 用来记录库的版本信息以及哈希值

清理库:go mod tidy,使用后会将未使用的库从 go.sum 中删除,并且自动引入使用的库