Go语言入门第二天-语言进阶 | 青训营笔记

140 阅读4分钟

Go语言入门第二天-语言进阶

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

一、语言进阶

1.并发 & 并行

并发:多线程程序在一个核的CPU上运行

并行:多线程程序在多个核的CPU上运行

而Go可以充分发挥多核优势,高效运行,就是为并发而生

1.1Goroutine

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。 线程:一个线程上可以跑多个协程,协程是轻量级的线程。

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

快速打印hello goroutine : 0~hello goroutine : 4

func hello(i int) {
    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.2 CSP(Communicating Sequential Processes)

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

Untitled.png 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

1.3 Channel

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

Untitled2.png

例子:

A子协程发送0-9数字

B子协程计算输入数字的平方

主协程输出最后的平方数

func CalSquare(){
		src :=make(chan int)
		dest :=make(chan int, 3)
		go func(){
				defer close(src)
				for i :=0; i<1-; i++{
						src <- i
				}
		}()
		go func(){
				defer close(dest)
				for i :=range src{
						dest <- i*i
				}
		}()
		for i := range dest {
				println(i)
		}
}

1.4 并发安全和Lock

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。

var x int64
var wg sync.WaitGroup

func add() {
    for i := 0; i < 5000; i++ {
        x = x + 1
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

1.5WaitGroup

WaitGroup对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait()用来控制计数器的数量。Add(n)把计数器设置为n ,Done()每次把计数器-1 ,wait()会阻塞代码的运行,直到计数器地值减为0。

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

2.依赖管理

学会站在巨人的肩膀上

2.1为什么需要依赖管理

最初的时候Go语言所依赖的所有的第三方包都放在 GOPATH 目录下面,这就导致了同一个包只能保存一个版本的代码,如果不同的项目依赖同一个第三方的包的不同版本,应该怎么解决呢?

Untitled3.png

2.2 go module

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

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是auto。

  • GO111MODULE=off禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包。
  • GO111MODULE=on启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据 go.mod下载依赖。
  • GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.mod和go.sum。

2.3 go mod命令

常用的go mod命令如下:

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

2.4 go get

在项目中执行go get命令可以下载依赖包,并且还可以指定下载的版本。

  • 运行go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
  • 运行go get -u=patch将会升级到最新的修订版本
  • 运行go get package@version将会升级到指定的版本号version 如果下载所有可以使用go mod download命令。

(1.17后使用go install)

3.测试

测试是避免事故的最后一道屏障

Untitled4.png

3.1 单元测试

不写测试的开发不是好程序员。我个人非常崇尚TDD(Test Driven Development)的,然而可惜的是国内的程序员都不太关注测试这一部分。 这篇文章主要介绍下在Go语言中如何做单元测试 和基准测试。

Untitled5.png

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

3.2 覆盖率

测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。

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

3.3 Mock

快速Mock函数

  • 为一个函数打桩
  • 为一个方法打桩
func Patch(target,replacement interface{}) *PatchGuard {
		t := reflect.ValueOf(target)
		r := reflect.ValueOf(replacement)
		patchValue(t, r)
		return
}
func Unpatch(target interface{}) bool {
		return unpatchValue(reflect.ValueOf(target))
}
func TestProcessFirstLineWithMock(t *testing.T) {
		monkey.Patch(ReadFirstLine, func() string {
				return "line110"
		})
		defer monkey.Unpatch(ReadFirstLine)
		line := ProcessFirstLine()
		assert.Equal(t, "line000", Line)
}

对ReadFirstLine打桩测试,不再依赖本地文件

3.4 基准测试

基准测试就是在一定的工作负载之下检测程序性能的一种方法。

基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。