GO语言学习第三节之高质量编程|青训营笔记

163 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。

并发操作

并发vs并行:

并发与并行
并发并行
多线程在一个CPU上运行多线程在多核cpu上运行

image.png

并行一般是实现并发的手段,并发可以最大程度的调度cpu资源,而go语言可以充分发挥多核优势,可以说Go语言就是为并发而生的。

Goroutine

协程与线程
协程线程
用户态,轻量级线程,栈KB级别,耗资源少。内核态,线程跑多个协程,栈MB级别。线程一般并发跑多个协程,最高可以支持跑几万个协程。

创建协程的方法: 在运行函数前面加go关键字 go func

func HelloGoRountine(){
   for i:=0;i<5;i++{
      go func(j int){
      hello(j)
      }(i)
   }
   time.Sleep(time.Second)
}

用sleep阻塞,保证子协程执行完之前,主协程不退出。但是这样的处理并不好,因为我们强行规定了sleep的时间,实际并不需要用这么多时间,还有更优秀的处理办法,也就是Waitgroup

Waitgroup:

比sleep更好的解决方式。Waitgroup本质上是计数器,开启协程时使用add协程数+1,结束用done提示协程数-1,阻塞器wait知道协程数为0才会关闭

通道channel

通道的声明使用make(chan 元素类型,[缓冲大小])

channel有两种:无缓冲通道与有缓冲通道。

无缓冲通道也被称为同步通道。同步输入输出。

解决同步问题的方法就是使用带有缓冲区的有缓冲通道,通道的容量代表通道中可以存储多少元素

并发安全Lock

在程序并发运行时,最好对临界区加锁保证程序的安全运行,否则可能输出错误的结果:

go语言提供了sync.MuteX来实现锁,其实就是操作系统中的互斥,防止同时对公共资源的调用导致错误

var 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
    }

我们使用go addwithlock()与go addwithoutlock()并行的创建五个协程来运行两个函数

Addwithlock函数对临界区实现了保护,最终输出了10000。

而Addwithoutlock函数没有对临界区保护,最终的输出值是个随机值,可能是正确值,也可能是小于10000的错误值。这个错误是概率发生的,因此在项目开发中很难定位,所以要对临界区做好保护。

依赖管理:

由于不同环境(项目)依赖的版本不同,所以我们需要对依赖进行管理,依赖管理从GOPATH到Go Vendor以及最新的Go Module,功能越来越强大。

环境变量GOPATH

GOPATH是我们在安装go语言的运行环境时就配置的环境变量,GOPATH主要包含了三个文件夹:

GOPATH
bin项目编译的二进制文件
pkg项目编译的中间产物,加速编译
src项目源码

项目代码直接依赖src下的代码

GOPATH的弊端:

如果一个项目在迭代前后依赖某一package的不同版本,GOPATH无法实现package的多版本控制

Go vendor

相比GOPATH,Go vendor在项目目录下增加vendor文件夹,vendor文件夹中是项目依赖的副本,通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题

Go vendor弊端:

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

Go Module

最新版本

  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包 类似Java中的maven
module example/project/app

go 1.16

require{
    example/lib v1.0.2
    example/lib2 v1.0.0 //indirect
    example/lib3 v0.1.0-20190725025543-5a5fe074e612
    example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd //indirect
    example/lib5/v3 v3.0.2
    example/lib6 v3.2.0 incompatible
}

第一行是模块路径,是依赖管理的基本单元,简单来说就是在哪里可以找到这个模块。

第二行是go原生库的版本。

Require是单元依赖

语义化版本

${MAJOR}.${MIINOR}.${PATCH}

例如:

  • V1.3.0
  • V2.3.1

MAJOR:大版本,不同的MAJOR版本代码隔离,可以不兼容

MINOR:小版本,一般是功能的增加,在MAJOR相同的情况下要做的代码兼容

PATCH:修复bug

Commit伪版本

vx.0.0-yyyymmddhhmmss-abcdergh1234

第一部分和语义化版本一致,第二部分是时间戳,第三部分是Go语言自动生成的版本号,是12位的哈希码

关键字

indirect

表示间接依赖,假如项目A依赖项目B,项目B依赖项目C,项目A对项目C就是间接依赖

incompatible

  • 主版本2+模块会在模块路径增加/vN后缀
  • 对于没有go.mod文件并且主版本2+的依赖,会+incompatible 以上面的go module为例,主版本是v1.0.2,对于lib5,它的版本是v3.0.2,所以要在后面加/v3

对于lib6,没有go.mod文件,且版本超过主版本2,所以要加incompatible

构建时会选择最低的兼容版本,因为1.3与1.4是MINOR版本迭代,所以代码兼容。

依赖分发:

依赖分发其实就是项目的依赖在哪里下载,如何下载

Proxy

因为我们使用的依赖可能被修改或者删除,这样就会对我们的项目造成影响。为了解决这个问题,就出现了Proxy,Proxy会缓存源站的内容,即使作者对依赖进行修改删除,也不会影响Proxy的内容。

GOPROXY也是一个环境变量,当我们使用proxy时,项目会首先去Proxy中寻找依赖,如果没找到就返回第三方源站

工具 go get/mod

go get命令

  • go get example.org/pkg @update 默认,不写也可以
  • go get example.org/pkg @none 删除依赖
  • go get example.org/pkg @v1.1.2 tag版本,语义版本
  • go get example.org/pkg @23dfdd5 特定的commit
  • go get example.org/pkg @master 分支最新的commit

go mod命令

  • go mod init:初始化,创建go.mod文件
  • go mod download: 下载模块到本地缓存
  • go mod tidy:增加需要的依赖,删除不需要的依赖

项目创建时就要用 go mod init初始化依赖

  依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/mod

测试

  1. 回归测试
  2. 集成测试
  3. 单元测试 从上到下,覆盖率逐渐变大,成本却逐渐降低。

go语言提供了专门的测试方式:

  • 所有的测试文件必须以_test.go结尾
  • func TestXxx(*tesing.t)
func HelloTom() string {
   return "Jerry"
}

func TestHelloTom(t *testing.T) {
   output := HelloTom()
   expectoutput := "Tom"
   if output != expectoutput {
      t.Errorf("Error,The output %s don't match expectoutput %s", output, expectoutput)
   }
}

测试例子:期望输出值是Tom,实际输出Jerry

测试输出结果: image.png

单元测试的覆盖率 image.png 显示单元测试覆盖率的指令是:

go test 测试程序 被测试程序 --cover

执行后会在下方显示测试率。

上面这段代码的测试覆盖率是66.7%,这是因为我们测试了输入值为70的情况下是否会输出true,但是没有测试在输入值小于60的情况下输出是否为false。因此最后一行代码的正确性没有得到检测。2/3就是66.7%

单元测试Tips

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

单元测试要隔离,也就是说测试要能在任何时间,在任何单元独立运行

所以我们需要用到mock机制,举个例子:

比如我们写了一串代码,需要打开文本文件,使用scan遍历并返回字符串,最终使用replaceall函数将字符串中的00全部替换为11。这个操作有一个前提条件,本地有这个文本文件,如果这个文本文件被篡改或删除,或者不在本地,就无法进行了。

这里就要用到mock

monkey下载地址: github.com/bouk/monkey 我们可以使用快速mock函数对一个函数或者方法打桩。 语法为:

func Patch(target,replacement interface{}) *PatchGuard

target就是原函数,replacement是打桩函数,我们在实际测试时调用的其实是打桩函数