Go进阶-工程实践 | 青训营笔记

80 阅读4分钟

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

1. 语言进阶

1.1 并发 VS 并行

你可能已对 Go 在并发方面的出色表现有所耳闻。 也许正是这一最突出的功能使 Go 如此受欢迎,让它成为了编写 DockerKubernetesTerraform 等其他软件的理想之选。

但是在开始了解 Go 并发之前,你可能需要忘记从其他编程语言中已经了解的知识,因为Go 使用的方法截然不同。

  1. 多线程程序在一个核的cpu上运行

image-20230116130558267

  1. 多线程程序在多个核的cpu上运行

    image-20230116130831560

Go可以充分发挥多核优势,高效运行

1.2 Goroutine

Goroutine 是轻量线程中的并发活动,而不是在操作系统中进行的传统活动。 假设你有一个写入输出的程序和另一个计算两个数字相加的函数。 一个并发程序可以有数个 Goroutine 同时调用这两个函数。

我们可以说,程序执行的第一个 Goroutinemain() 函数。 如果要创建其他 Goroutine ,则必须在调用该函数之前使用 go 关键字:

func main(){
   go launch()
}
package main

import (
	"fmt"
	"time"
)

func main() {
    HelloGoroutine()
}
// 打印当前数值
func hello(i int) {
	fmt.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)
}

1.3 Channel

Go 中的 channelgoroutine 之间的通信机制。 这就是为什么我们之前说过 Go 实现并发的方式是:“不是通过共享内存通信,而是通过通信共享内存。”需要将值从一个 goroutine 发送到另一个时,可以使用通道。

由于 channel 是发送和接收数据的通信机制,因此它也有类型之分。这意味着你只能发送 channel 支持的数据类型。 除使用关键字 chan 作为 channel 的数据类型外,还需指定将通过 channel 传递的数据类型,如 int 类型。

// 基本形式
make(chan type)
// 无缓冲通道
make(chan int)
// 有缓冲通道
make(chan int, 2)
// 缓冲通道可以理解为可以同时有几个线程来执行

一个 channel 可以执行两项操作:发送数据和接收数据。 若要指定 channel 具有的操作类型,需要使用 channel 运算符 <-

如果希望 channel 仅发送数据,则必须在 channel 之后使用 <- 运算符,如果希望 channel 接收数据,则必须在 channel 之前使用 <- 运算符此外,而在 channel 中发送数据和接收数据属于阻止操作。

ch <- x // 通过通道ch发送(或写入)数据x
x = <-ch // x接收(或读取)数据,发送到通道ch
<-ch // 接受数据,但是结果被丢弃,相当于临时变量

最后是关闭通道,和其他语言中的文件操作一样,用完需要进行最后的关闭操作,释放资源。

close(ch)
package main

func main() {
    // 创建无缓冲通道
    src := make(chan int)
    // 创建有缓冲通道
    dest := make(chan int, 3)
    go func() {
        // 最后执行关闭通道
        defer close(src)
        // 发送0~9到通道src中
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    go func() {
        // 最后执行关闭通道
        defer close(dest)
        // 从通道src中取数据,并将数据平方后存入通道dest中
        for i := range src {
            dest <- i * i
        }
    }()
    // 输出最后的平方数
    for i := range dest {
        println(i)
    }
}

1.4 并发安全Lock

多线程中都有的问题,并发中的线程安全问题,然后保证多个线程不会相互干扰,锁的应用就出来了。

package main

import (
    "sync"
    "time"
)
// 创建全局变量
var (
    x    int64
    lock sync.Mutex
)
// 对变量进行2000次的+1操作,并在每次操作都加上锁,保证不会有其他线程影响本线程操作
func addWithLock() {
    for i := 0; i < 2000; i++ {
        // 加锁
        lock.Lock()
        x += 1
        // 解锁
        lock.Unlock()
    }
}
// 同样对变量进行2000次的+1操作,但是不加锁
func addWithoutLock() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}
func main() {
    // 初始化变量
    x = 0
    // 创建5个协程并发执行
    for i := 0; i < 5; i++ {
        addWithoutLock()
    }
    time.Sleep(time.Second)
    println("WithoutLock:", x)// 这个看情况,一般小于10000
    // 重置
    x = 0
    // 创建5个协程并发执行
    for i := 0; i < 5; i++ {
        addWithLock()
    }
    time.Sleep(time.Second)
    println("WithLock:", x)// 10000
}

2. 依赖管理

2.1 Go依赖管理演进

Go的依赖管理分别经历了GOPATH -> Go Vender -> Go Module,目前被广泛采用的是Go Module,整个演进路线主要围绕两个目标来迭代发展:不同环境(项目)依赖的版本不同、控制依赖库的版本。

2.1.1 GOPATH

GOPATH是Go语言支持的一个环境变量,有以下结构:

  • src:存放Go项目的源码
  • pkg:存放编码的中间产物,加快编译速度
  • bin:存放Go项目编译生成的二进制文件

弊端:

如果多个项目依赖同一个库,则依赖该库的同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足项目依赖需要,为解决该问题,Go Vender出现了。

2.1.2 Go Vendor

vendor是当前项目中的一个目录,其中存放了当前项目依赖的版本。在Vendor机制下,如果当前项目存在Vendor目录,会优先使用该目录下的依赖,如果依赖不存在,会从GOPATH中寻找。但vender不能很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题。

2.1.3 Go Module

Go Moldules是GO语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸加无法依赖同一个库的多个版本等问题,通过go.mod文件管理依赖包版本,通过go get/go mod指令工具管理依赖包。

暂时写到这里。。。