Go 语言进阶的必备技能

Go 语言从入门到工程实践~

前言:

经过了第一天的学习,基本掌握了 Golang 的基础语法和一些常用的标准库使用。在本次课程中,讲师紧接着前一天的内容,带领大家从工程实践的角度,分享了 Go 语言进阶过程中涉及的几大必备技能:并发编程、依赖管理、软件测试方法 …

在本次的课程结尾,还准备了一个简单的项目实战,从真实的项目案例中抛出一个需求模型,带领我们验到了真实的项目开发流程。

Go 语言进阶的必备技能_第1张图片


并发编程

如果学过操作系统这门课,提到并发编程很快会想到进程与线程,线程相比于进程更加轻量。然而 Golang 中,又在线程的基础上进一步提出了协程 (Goroutine)的概念,协程被称为“用户态线程”,不存在 CPU 上下文切换的问题,效率非常高。

Go 语言进阶的必备技能_第2张图片

协程的引入,使得开发者可以很容易的用 Go 写出高并发的程序。

1. 协程实例 - Hello Goroutine:

Go 语言中开启一个协程非常简单,只需要在调用的函数前面加上 go 关键字,就可以让 Go 为该函数创建一个协程来运行。

下面是一个协程创建的实例:

package main

import (
    "fmt"
    "time"
)

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

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

主函数中开启了 5 个协程用来并发打印 "hello goroutine : i",按照预期会输出五条乱序的语句。

hello goroutine : 4
hello goroutine : 0
hello goroutine : 1
hello goroutine : 2
hello goroutine : 3
2. 协程间通信 - Channel:

我们都知道线程间的通信可以使用共享内存的方式,但这种方式会给多线程带来一些问题,所以 Go 提倡通过 通信共享内存 而不是通过共享内存而实现通信。Go 使用通道(Channel)来实现协程间的通信,通道是一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱。

Go 语言进阶的必备技能_第3张图片

使用 make(chan 元素类型, [缓冲大小]) 可以创建一个通道,通道(Channel)分为两种类型:

  • 无缓冲通道 —— make(chan int)
  • 有缓冲通道 —— make(chan int, 2)

Go 语言进阶的必备技能_第4张图片

无缓冲的通道保证同时交换数据,而有缓冲的通道不做这种保证。

下面通过一个例子来看一下 Channel 如何使用,在该例子中,A 子协程发送 0~9 数字,B 子协程计算输入数字的平方,主协程输出最后的平方数。

package main

import "fmt"

func main() {
    src := make(chan int)
    dest := make(chan int, 3)
    // A子协程
    go func() {
        defer close(src)    // 延迟资源关闭
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    // B子协程
    go func() {
        defer close(dest)    // 延迟资源关闭
        for i := range src {
            dest <- i * i
        }
    }()
    // 主协程
    for i := range dest {
        fmt.Print(i, " ");
    }
}

该程序的运行结果如下:

0 1 4 9 16 25 36 49 64 81

上面的例子可以抽象成生产者与消费者问题,从结果中可以看出通过 Channel 这种方式,是能保证协程间的运行顺序的,也就是并发安全的。

3. 并发安全 Lock:

Go 语言保留了通过共享内存来实现协程通信的机制,在这种方式下可能存在多个 Goroutine 同时操作一块内存资源的情况,也就是会发生数据竞态。

要确保并发安全,需要对临界资源进行加锁,Golang 中使用 lock.Lock()lock.Unlock() 来对临界区加锁和释放。

在下面的示例中,会开启 5 个协程来执行变量 x 自增 2000 次的操作,分别在加锁与不加锁的情况下,来看一下程序的运行结果:

package main

import (
	"sync"
	"time"
)

var (
    x    int64
    lock sync.Mutex
)

// 对变量x的访问加锁
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 main() {
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    println("WithoutLock: ", x)    // 预期结果:10000
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("WithLock: ", x)    // 预期结果:10000
}

运行结果:

WithoutLock:  6731
WithLock:  10000

可以发现多个协程访问同一个资源可能得不到期望结果,因此要使用互斥锁来避免非并发安全的读写操作。

4. 语言等待组 WaitGroup:

Go 语言中除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务。

WaitGroup 有以下几个方法:

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

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

我们可以使用 WaitGroup 来改写上面的协程实例 - Hello Goroutine:

package main

import (
    "fmt"
    "sync"
)

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

func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}

依赖管理

项目开发中经常需要引入第三方依赖,不同环境依赖的版本可能不同,为了解决这一问题,诞生了很多依赖管理工具 GOPATH、Go Vender、Go Module,这些工具让我们可以方便地控制依赖库地版本。

Go 语言进阶的必备技能_第5张图片

1. GOPATH:

GOPATH 是 Go 语言支持的环境变量,是项目的工作区,主要有三个主要的目录:

Go 语言进阶的必备技能_第6张图片

src 目录是存放项目源代码的地方,GOPATH 会将所有的依赖都放在 src 目录下,通过 go get 指令来下载最新版本的包到 src 目录下。

由于 GOPATH 不支持依赖的多版本控制,多个项目有相同的依赖时可能会产生问题,因此后面又产生了 Go Vendor。

2. Go Vendor:

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。

Go 语言进阶的必备技能_第7张图片

Go Vendor 仍然存在以下问题:

  • 无法控制依赖的版本。
  • 更新项目又可能出现依赖冲突,导致编译出错。
3. Go Module:

Go Module 是 Go 语言官方推出的依赖管理系统,解决了之前工具的诸多问题。目前最新版本的 Golang 已经默认开启了 Go Module 的管理能力。

  • 通过 go.mod 文件管理依赖包版本。
  • 通过 go get/go mod 指令工具管理依赖包。

官方文档传送门:Modules · golang/go Wiki (github.com)

依赖配置 - go.mod:

类似于 Java 语言的依赖管理工具 Maven,Go Module 使用 go.mod 文件来管理项目的依赖包信息,格式如下:

module github.com/gosoon/audit-webhook

go 1.12 

require ( 
    github.com/elastic/go-elasticsearch v0.0.0 
    github.com/gorilla/mux v1.7.2 
    github.com/gosoon/glog v0.0.0-20180521124921-a5fbfb162a81 
)

常用命令:

GO MODULE 常用命令 描述
go mod init 初始化 go.mod
go mod tidy 更新依赖文件
go mod download 下载依赖文件
go mod vendor 将依赖转移至本地的 vendor 文件
go mod edit 手动修改依赖文件
go mod graph 打印依赖图
go mod verify 校验依赖

Go Module 使用流程:

  1. 初始化项目:进入项目目录,使用 init 指令后会生成一个 go.mod 文件,它的内容将会被 go toolchain 全面掌控,go toolchain 会在各类命令执行时,比如 go getgo buildgo mod 等修改和维护 go.mod 文件。
    mkdir Project
    cd Project
    go mod init Project
    
  2. 添加依赖:在 main.go 文件的 import() 代码块中添加第三方依赖,执行 go run main.go 运行代码会发现 go mod 会自动查找依赖自动并下载,同时 go.mod 文件也会随着变化。
    package main
    
    import (
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        // ...
    }
    
  3. 使用 go get 指令升级依赖:运行 go get -u 将会升级到最新的次要版本或者修订版本;运行 go get -u=patch 将会升级到最新的修订版本;运行 go get package@version 将会升级到指定的版本号 version。

软件测试

软件测试关系着系统的质量,测试是避免事故的最后屏障,通过软件测试可以发现程序中的各种问题和错误,规避软件事故造成的资金损失。

测试分为回归测试、集成测试与单元测试,覆盖率逐渐变大但是成本逐渐降低。单元测试主要是开发阶段开发者对某个模块进行测试,单元测试一定程度上决定了代码的质量,本次课程也着重介绍了单元测试。

Go 语言进阶的必备技能_第8张图片

单元测试:

1. 单元测试 - 规则:

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

2. 单元测试 - 运行:

GoLand 中可以右键直接运行单元测试,VS Code 可以在终端中使用 go test [flags] [packages] 指令。

3. 单元测试 - Tips:

  • 单元测试要保证较高的覆盖率,80% 以上。
  • 测试分支相互独立,全面覆盖。
  • 测试单元粒度足够小,函数单一职责。
单元测试 - Mock:

Mock 方法是单元测试中常见的一种技术,它的主要作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。

使用 Mock 做接口测试时,一般分二步:

  1. 打桩:创建 Mock 桩,指定 API 请求内容及其映射的响应内容。
  2. 调桩:被测服务来请求 Mock 桩并接收 Mock 响应。

Go 常用开源 Mock 包:bouk/monkey: Monkey patching in Go (github.com)

基准测试:

Go 语言提供了基准测试的组件,基准测试是指测试程序运行时的性能,比如 CPU 的资源开销等。

testing 包中内置了基准测试的功能,在使用 go test 命令时,默认会将基准测试排除,只运行单元测试,所以需要在 go test 命令后加上 -bench 以执行基准测试。-bench 标记使用一个正则表达式来匹配要运行的基准测试函数名称。所以,最常用的方式就是通过 -bench=. 标记来执行该包下的所有的基准函数。

% go test -bench=. ./examples/test/

项目实战 - 组件及技术点

在课程结尾讲师通过 “社区话题页面” 案例展示了开发过程中涉及到的技术点以及一些常用组件,在此做一个总结。

项目的分层结构:

Go 语言进阶的必备技能_第9张图片

在实际的项目开发中,通常采用分层开发的方式,上图的分层模型就是我们常说的 MVC 模型,不同层实现不同的功能,实现了项目的分层解耦,有利于开发团队的分工合作。

分层结构主要分为三个部分:

  • 数据层:外部数据的增删改查(CRUD)。
  • 逻辑层:处理核心业务逻辑输出。
  • 视图层:处理和外部的交互逻辑。
组件工具:

1. Gin 框架:

Gin 是一个高性能的开源 go web 框架,用来开发后端程序。

传送门:Gin Web Framework (gin-gonic.com)

2. Go Mod:

通过依赖管理来引入 Gin 框架:

go mod init
go get gopkg.in/gin-gonic/[email protected]

你可能感兴趣的:(golang,测试工具,java)