Go进阶知识点学习|青训营笔记

125 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天,今天学习了Go并发特性,依赖管理,测试等知识,下面是我的笔记

Go语言进阶与工程实践

1、Go并发特性

1.1、协程和Goroutine

  • 协程的定义和作用

协程是比线程更加轻量级的存在,它就是一个可以在某个地方挂起的特殊函数,并且可以重新在挂起处继续运行。所以说,协程与进程、线程相比,不是一个维度的概念。

一个进程可以包含多个线程,一个线程也可以包含多个协程,也就是说,一个线程内可以有多个那样的特殊函数在运行。

一个线程内的多个协程的运行是串行的。如果有多核CPU的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内的多个协程却绝对串行的,无论有多少个CPU(核)。因为协程虽然是一个特殊的函数,但仍然是一个函数。

一个线程内可以运行多个函数,但是这些函数都是串行运行的。当一个协程运行时,其他协程必须挂起。

进程、线程、协程

进程线程协程
切换者ososuser
切换时机by osby osby user
切换内容刷新tlb,内核栈,context上下文内核栈,上下文硬件上下文
切换内容的保存内核栈内核栈用户堆栈
切换过程用户态-内核态-用户态用户态-内核态-用户态用户态(不会进入内核态)
切换效率略高最高

协程使用场景:

io阻塞性,单核cpu,不适用与io密集型,需要并发性能的场景,因为协程本质上是串行化的。

goroutine就是基于协程实现的

  • goroutine的使用,下面通过一段代码来展示

func DelayPrint() {
	for i := 1; i <= 4; i++ {
		time.Sleep(250 * time.Millisecond)
		fmt.Println(i)
	}
}

func HelloWorld() {
	fmt.Println("Hello world goroutine")
}

func main() {
	go DelayPrint()    // 开启第一个goroutine
	go HelloWorld()    // 开启第二个goroutine
	time.Sleep(2*time.Second)
	fmt.Println("main function")
}
main函数执行不关心goroutine是否结束
,且会强制退出所有未完成的goroutine函数

1.2、channel

  • Go提倡通过通信共享内存而不是共享内存来通信

  • Go语言使用channel来进行通信

  • channel的定义方法如下

    var ch chan int // 声明一个传递int类型的channel ch := make(chan int) // 使用内置函数make()定义一个channel,显然内部参数是channel的存入元素的类型。

    //=====================================

    ch <- value // 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据 value := <-ch // 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止

    //=======================================

    close(ch) // 关闭channel 使用<-符号负责传输,无缓冲默认会阻塞的

    //=======================================

    ch := make(chan string, 3) // 创建了缓冲区为3的通道

    带缓冲区的channel,可以解决生产消费者模式的效率问题。

    make为初始化操作,注意如果在main函数外面则必须用var echo chan string的方法来声明channel,但是使用之前必须make一下。 //========= len(ch) // 长度计算 cap(ch) // 容量计算

1.3、sync

​ sync设计了众多线程安全的锁,这里介绍了

  • sync.Mutex

  • sync.WaitGroup

    Mutex一种互斥锁。互斥体的零值是一个解锁的互斥体。

    互斥体在第一次使用后不得复制。在Go内存模型的术语中,对于任何n < m的情况,第n次调用Unlock“先于”第m次调用lock。成功调用TryLock等同于调用Lock。对TryLock的调用失败根本不会建立任何“之前同步”关系。

    func (m *Mutex) Lock()
    

    如果锁已经被使用,调用的goroutine将阻塞,直到互斥锁可用。

    func (m *Mutex) TryLock() bool
    

    TryLock尝试锁定m并报告是否成功。

    func (m *Mutex) Unlock()
    

    解锁m。如果m在进入解锁状态时未被锁定,则出现运行时错误。

`

waitgroup类型是计数器,有三个主要方法:

func (wg *WaitGroup) Add(delta int)

​ Add向WaitGroup计数器添加增量,增量可能为负值。如果计数器变为零,所有阻塞等待的goroutines将被释放。如果计数器为负,则增加panic。

​ Done将WaitGroup计数器减1。

	func (wg *WaitGroup) Wait()

​ 等待块,直到WaitGroup计数器为零。

//将Done函数放在协程的开头defer一下,能保证减一的操作,代码如下。

go func addWithLock() {
   defer ss.Done()
   for i := 0; i < 2000; i++ {
      lock.Lock()
      x += 1
      lock.Unlock()
   }
}()

2、Go的依赖管理

2.1、依赖管理演进

Gopath-->Govendor-->Go module

2.2、Gopath的缺陷

​ 项目代码直接依赖于src包,只能在src下建项目。

​ 缺陷

  • 同一个包依赖冲突
  • 版本控制困难
  • 只能在src包下编程,强迫症难受,不灵活。

2.3、Govendor

​ 项目目录下增加vendor文件,所有依赖包副本形式放在ProjectRoot/vendor中,

vendor=>GOPATH,解决了多个项目需要同一个包的依赖冲突问题。

  • 但是存在不同版本的依赖冲突问题

2.4、Go Module

  • go.mod文件管理依赖版本

  • 中心仓库管理依赖库Proxy

  • 本地工具 go get/mod

    类似于java中的maven。

3、测试

3.1、Go语言单元测试及基准测试

​ Go语言配置了单元测试相关的库,/test,编写test测试的主要规则有:

  • 所有测试文件以_test.go结尾

  • func TestXxx(*testing.T)

  • 初始化逻辑放到TestMain中

    func Sum(arr []int) int {
    	res := 0
    	for _, value := range arr {
    		res += value
    	}
    	return res
    }
    
    func TestSum(t *testing.T) {
       encode := Sum([]int{1, 23, 3})
       expectedVal := 25
       if encode != expectedVal {
          t.Error("这是一个错误")
       }
    }
    --------------------------------
    === RUN   TestSum
        main_test.go:9: 这是一个错误
    --- FAIL: TestSum (0.00s)
    
    FAIL
    

func BenchmarkXxx(*testing.B)

被视为基准,当提供了-bench标志时,由“go test”命令执行。基准是按顺序运行的。

一个示例基准函数如下所示:

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

基准函数必须运行目标代码b.N次。在基准执行期间,b.N被调整,直到基准函数持续足够长的时间来可靠地计时。输出

BenchmarkRandInt-8   	68453040	        17.8 ns/op

意味着该循环以每循环17.8 ns的速度运行了68453040次。

如果基准测试在运行前需要一些昂贵的设置,可以重置计时器:

func BenchmarkBigLen(b *testing.B) {
    big := NewBig()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        big.Len()
    }
}

3.2、开源测试包assert

go get "github.com/stretchr/testify/assert"

​ 通过以上命令获取该包进行测试,assert包下封装了一些常用的测试手段。

assert.Equal(t, encode, expectedVal, "这是一个错误")

3.3、单元测试的覆盖率

​ go test _test,go --cover 显示覆盖率

  • 一般覆盖率:50%-80%
  • 测试分支相互独立、全面覆盖。
  • 测试单元粒度足够小,函数单一职责。

3.4、Mock测试

mock测试运用的场景:

  • 需要将当前被测单元和其依赖模块独立开来,构造一个独立的测试环境,不关注被测单元的依赖对象,只关注被测单元的功能逻辑。

  • 被测单元依赖的模块尚未开发完成,而被测单元需要依赖模块的返回值进行后续处理。

  • 前后端项目中,后端接口开发完成之前,接口联调;

  • 依赖的上游项目的接口尚未开发完成,需要接口联调测试;

  -----比如service层的代码中,包含对Dao层的调用,但是,DAO层代码尚未实现

  • 被测单元依赖的对象较难模拟或者构造比较复杂。