Go语言进阶学习(1月16日) | 青训营笔记

145 阅读9分钟

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

一、本堂课重点内容:

  1. Go语言进阶
  2. Go语言依赖管理
  3. Go语言测试
  4. Go语言的实战案例

二、详细知识点介绍:

1. Go语言进阶

今天学习go语言进阶,从下面来介绍今天学到的并发编程:

首先我们先了解一下(并发&并行)

  • 并发:多个线程程序在一个核的CPU上的运行
  • 并行:多个线程程序在多个核的CPU上的运行(Go语言可以充分发挥多核优势,高效运行,这就是我们常常说的Go支持高并发是十分强大的)

1.1 Goroutine

  • Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。
  • 协程是运行在内核态的,线程跑多个协程,栈KB级别。线程则是在用户态,轻量级线程,栈MB级别。由下图所示:

image.png

  • 用法:

Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。

(1)格式

为一个普通函数创建 goroutine 的写法如下:

go 函数名( 参数列表 )
  • 函数名:要调用的函数名。

  • 参数列表:调用函数需要传入的参数。

使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。

如果需要在 goroutine 中返回数据,请使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。

(二)使用匿名函数创建goroutine的格式

使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下:

go func( 参数列表 ){  
    函数体  
}( 调用参数列表 )

其中:

  • 参数列表:函数体内的参数变量列表。
  • 函数体:匿名函数的代码。
  • 调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。

1.2 CSP(Communicating Sequential Processes)

通过通信共享内存:在Go开发中,提倡通过通过通信共享内存,而不是通过共享内存而实现通信。

1.3 Channel

Go语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

  • 特性

    Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

    通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

1.4 并发安全 Lock

Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。

  • Go语言互斥锁(sync.Mutex)

    Mutex 是最简单的一种锁类型,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。

  • Go语言读写互斥锁(sync.RWMutex)

    RWMutex 相对友好些,是经典的单写多读模型。在读锁占用的情况下,会阻止写,但不阻止读,也就是多个 goroutine 可同时获取读锁(调用 RLock() 方法;而写锁(调用 Lock() 方法)会阻止任何其他 goroutine(无论读和写)进来,整个锁相当于由该 goroutine 独占。从 RWMutex 的实现看,RWMutex 类型其实组合了 Mutex。

对于这两种锁类型,任何一个 Lock() 或 RLock() 均需要保证对应有 Unlock() 或 RUnlock() 调用与之对应,否则可能导致等待该锁的所有 goroutine 处于饥饿状态,甚至可能导致死锁。

1.5 WaitGroup

Go语言中除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务 在 sync.WaitGroup(等待组)类型中,每个 sync.WaitGroup 值在内部维护着一个计数,此计数的初始默认值为零。

等待组有下面几个方法可用,如下表所示。

方法名功能
(wg * WaitGroup) Add(delta int)等待组的计数器 +1
(wg * WaitGroup) Done()等待组的计数器 -1
(wg * WaitGroup) Wait()当等待组计数器不等于 0 时阻塞直到变 0。

对于一个可寻址的 sync.WaitGroup 值 wg:

  • 我们可以使用方法调用 wg.Add(delta) 来改变值 wg 维护的计数。

  • 方法调用 wg.Done() 和 wg.Add(-1) 是完全等价的。

  • 如果一个 wg.Add(delta) 或者 wg.Done() 调用将 wg 维护的计数更改成一个负数,一个恐慌将产生。

  • 当一个协程调用了 wg.Wait() 时,

    • 如果此时 wg 维护的计数为零,则此 wg.Wait() 此操作为一个空操作(noop);

    • 否则(计数为一个正整数),此协程将进入阻塞状态。当以后其它某个协程将此计数更改至 0 时(一般通过调用 wg.Done()),此协程将重新进入运行状态(即 wg.Wait() 将返回)。

等待组内部拥有一个计数器,计数器的值可以通过方法调用实现计数器的增加和减少。当我们添加了 N 个并发任务进行工作时,就将等待组的计数器值增加 N。每个任务完成时,这个值减 1。同时,在另外一个 goroutine 中等待这个等待组的计数器值为 0 时,表示所有任务已经完成。

2. 依赖管理

对于hello word以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0-1编码搭建,而更多的关注业务逻绿的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过SDK的方式引入,这样对依赖包的管理就显得十分重要!

三个版本:

image.png

2.1 GOPATH

GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区

目录有以下结构:

  • src: 存放Go项目的源码;

  • pkg: 存放编译的中间产物,加快编译速度;

  • bin: 存放Go项目编译生成的二进制文件;

弊端

在GOPATH管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足实际开发需求!

于是出现了下面的Go Vendor。

2.2 Go Vendor

Vendor是当前项目中的一个目录,其中存放了当前项目依赖的副本。在Vendor机制下,如果当前项目存在Vendor目录,会优先使用该目录下的依赖,若依赖不存在,则会从GOPATH中寻找。

弊端

  • 无法控制依赖的版本
  • 更新项目有可能出现依赖冲突,导致编译出错

2.3 Go Module

go module 是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。

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

image.png

image.png

常用的 go mod 命令如下表所示:

命令作用
go mod download下载依赖包到本地(默认为 GOPATH/pkg/mod 目录)
go mod edit编辑 go.mod 文件
go mod graph打印模块依赖图
go mod init初始化当前文件夹,创建 go.mod 文件
go mod tidy增加缺少的包,删除无用的包
go mod vendor将依赖复制到 vendor 目录下
go mod verify校验依赖
go mod why解释为什么需要依赖

GOPROXY

proxy 顾名思义就是代理服务器的意思。大家都知道,国内的网络有防火墙的存在,这导致有些Go语言的第三方包我们无法直接通过go get 命令获取。GOPROXY 是Go语言官方提供的一种通过中间代理商来为用户提供包下载服务的方式。要使用 GOPROXY 只需要设置环境变量 GOPROXY 即可。

目前公开的代理服务器的地址有:

  • goproxy.io

  • goproxy.cn:(推荐)由国内的七牛云提供。

Windows 下设置 GOPROXY 的命令为:

   go env -w GOPROXY=https://goproxy.cn,direct

3. 测试

3.1 测试规则

  • 所有测试文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain中

3.2 依赖

3.3 文件处理

3.4 Mock

3.5 基准测试

4. 项目实战

三、实践练习例子:

1. Go语言进阶

1.1 Goroutine

func hello(i int) {
   println("hell0+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()
}

1.3 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 {
      fmt.Println(i) //打印操作
   }
}
func main() {
   Calsquare()
}

1.4 并发安全Lock

对变量执行2000次+1操作,5个协程并发执行

package main

import (
   "sync"
   "time"
)

var (
   x    int64
   lock sync.Mutex
)

func addWithLock() {
   for i := 0; i < 2000; i++ {
      lock.Lock()
      x += 1
      lock.Unlock()
   }
}
func addWithoutLock() {
   for i := 0; i < 2000; i++ {
      x += 1
   }
}
func Add() {
   x = 0
   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
   time.Sleep(time.Second)
   println("不加锁 WithoutLock:", x)
   x = 0
   for i := 0; i < 5; i++ {
      go addWithLock()
   }
   time.Sleep(time.Second)
   println("加锁 WithLock:", x)
}
func main() {
   Add()
}

image.png

1.5 WaitGroup

func hello(i int) {
   println("hell0+goroutine:" + fmt.Sprint(i))
}

func HelloGoRoutine() {

   var wg sync.WaitGroup
   wg.Add(5) //添加5个协程
   for i := 0; i < 5; i++ {
      go func(j int) { // 开启一个协程!
         defer wg.Done()
         hello(j)
      }(i)
   }
   //time.Sleep(time.Second)
   wg.Wait()
}

四、课后个人总结:

经过本次的学习,我学会了Go语言的高级用法,学到了协程、互斥锁等知识,之后我学习了项目的依赖管理,了解了依赖管理的三个版本历史,和现在最常用的依赖管理方法,最后我学习了测试的各自方法,通过测试,可以提高我们的代码质量,健壮我们的程序! 最后,通过一个项目实战,巩固了今天所学,今天还是收获慢慢,得多练习练习才能学的更精!

五、引用参考: