go语言进阶 | 青训营

90 阅读3分钟

1并发

协程:用户态,轻量级线程,栈KB级别

线程:内核态,线程跑多个协程,栈MB级别

1.1CSP

CSP的理念:以通信的方式来共享内存

这就是 Go 的并发哲学,它依赖 CSP 模型,基于 channel 实现。Go 一开始就把 CSP 的思想融入到语言的核心里,所以并发编程成为 Go 的一个独特的优势。

大多数的编程语言的并发编程模型是基于线程和内存同步访问控制,Go 的并发编程的模型则用 goroutine 和 channel 来替代。Goroutine 和线程类似,channel 和 mutex (用于内存同步访问控制)类似。

Golang中channel 是被单独创建并且可以在进程之间传递,它的通信模式类似于 boss-worker 模式的,一个实体通过将消息发送到channel 中,然后又监听这个 channel 的实体处理,两个实体之间是匿名的,这个就实现实体中间的解耦,其中 channel 是同步的一个消息被发送到 channel 中,最终是一定要被另外的实体消费掉的,在实现原理上其实类似一个阻塞的消息队列。

1.2go中的Goroutine

Goroutine 是Golang实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,类似于 greenthread,go底层选择使用coroutine的出发点是因为,它具有以下特点:

  • 用户空间 避免了内核态和用户态的切换导致的成本。
  • 可以由语言和框架层进行调度。
  • 更小的栈空间允许创建大量的实例。

Golang中的Goroutine的特性: Golang内部有三个对象: P对象(processor) 代表上下文(或者可以认为是cpu),M(work thread)代表工作线程,G对象(goroutine)。

  • G(Goroutine):我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息。
  • M(Machine):对内核级线程的封装,数量对应真实的CPU数(真正干活的对象)。
  • P(Processor):即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS()来设置,默认为核心数

构建

通过go关键字调用底层函数runtime.newproc()创建一个goroutine,当调用该函数之后,goroutine会被设置成runnable状态

func main() {
   go func() {
    fmt.Println("func routine")
   }()
   fmt.Println("main goroutine")
}

1.3channel

Golang是为并发而生的语言,Go语言是为数不多的在语言层面实现并发的语言;也正是Go语言的并发特性,吸引了全球无数的开发者。

Golang的CSP并发模型,是通过Goroutine和Channel来实现的。

Goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。 Channel是Go语言中各个并发结构体(Goroutine)之前的通信机制。通常Channel,是各个Goroutine之间通信的”管道“,有点类似于Linux中的管道。

通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。

在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。

而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。

语法

make(chan元素类型,[缓冲大小])
无缓冲通道make(chan int,)
有缓冲通道make(chan int,2)
package main

func main() {
    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)
    }
    //第一个线程将i传给src,第二个线程使用src得到数据传给dest
}

17.4并发安全Lock

导包

import (
    "sync"
)
互斥锁Mutex

互斥锁是一种常用的控制共享资源的办法,它能够同时保证一个gotroutine可以访问共享资源。go语言中使用sync和metux类型来实现互斥锁。

//别名
var 变量 lock sync.Mutex
//加锁
lock.Lock()
//释放
lock.Unlock()
读写互斥锁RWMutex

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果获取读锁则会继续获取锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是读锁还是写锁都会等待。

var rwlock sync.RWMutex//别名

rwlock.Lock() //加写锁
rwlock.Unlock() //解写锁

rwlock.RLock() //加读锁
rwlock.RUnlock() //解读锁
WaitGroup

go语言使用sync.WaitGroup来实现并发同步

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

var wg sync.WaitGroup//别名
 
wg.Add(1)//计数器+1
wg.Done()//计数器-1
wg.Wait()//等到直到计数器变为0

2依赖管理

2.1依赖管理进程

GOPATH

  1. 环境变量
    • bin 项目编译二进制文化
    • pkg 项目中间产物
    • src 项目源码
  2. 项目代码直接依赖src下代码
  3. go get 下载最新版本包到src下

无法实现多版本控制

Go Vendor

  1. 项目目录下加vendor文件夹,依赖包放在$ProjectRoot/vendor
  2. 寻址方式:vendor=>GOPATH
  3. 解决多个项目依赖同一个package

无法控制依赖版本

可能有依赖冲突导致编译错误

Go Module

  1. 通过go.mod控制依赖包版本
  2. 通过go get/go mod 指令工具管理依赖包

2.2依赖管理三要素

  • go.mod:配置文件,描述依赖。
  • Proxy:代理镜像地址,存储着中心仓库的副本,如果没有会从中心仓库下载。
  • go get/mod:本地工具,用于获取需要的依赖。

依赖配置

go.mod

命令行运行 go mod init + 模块名称 初始化模块

运行完之后,会在当前目录下生成一个go.mod文件,这是一个关键文件,之后的包的管理都是通过这个文件管理。

go get 命令 //下载gorose包到 $GOPATH/src

go.mod文件一旦创建后,它的内容将会被go toolchain全面掌控。go toolchain会在各类命令执行时,比如go get、go build、go mod等修改和维护go.mod文件。

go.mod 文件内提供了module, require、replace和exclude四个关键字,这里注意区分与上表中命令的区别,一个是管理go mod命令,一个是go mod文件内的关键字。

  • module语句指定包的名字(路径)
  • require语句指定的依赖项模块
  • replace语句可以替换依赖项模块
  • exclude语句可以忽略依赖项模块
version

语义化版

${MAJOR}.${MINOR}.${PATCH}
v1.3.0

基于commit伪版本

vx.0.0-yyyymmddhhmmss-abcdefgh1234
indirect

间接依赖的标识

incompatible

对于没有go.mod文件,且版本为2+的依赖使用

依赖分发

回源
Proxy
变量GOPROXY
GOPROXY="https://proxy1.cn,https://proxy2.cn,direct"

工具

go get
go get linxi.com/pkg
  • @updaate 默认
  • @none 删除依赖
  • @v1.1.2 语言版本
  • @23dfdd5 特定commit
  • @master 分支最新commit
go mod
go mod init  初始化得到go.mod
go mod download  下载模块到本地
go mod tidy  增加依赖,删除不需要的依赖

3测试

作用: 避免事故

单元测试->集成测试->回归测试

  • 覆盖边小,但成本增加

3.1单元测试

规则

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

命令

go test xxx_test.go

3.2assert包

导包

improt("github.com/stretchr/testify/assert")

方法

assert.Equal(t,"错误后返回值",正确结果)

3.3依赖测试

外部依赖=>稳定&幂等

文件处理
open, err := os.Open("log")//打开文件

scanner := bufio.NewScanner(open)//根据二进制文件字节建立读取器

scanner.Scan()//读取文件内容

3.4单元测试Mock

导包

improt("https://github.com/bouk/monkey")
Patch(方法名,replacement interface{})为一个函数打桩
Unpatch(方法名)为一个方法打桩

不在依赖本地文件