Go 语言进阶的必备技能

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

前言:

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

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

image.png


并发编程

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

image.png

协程的引入,使得开发者可以很容易的用 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)来实现协程间的通信,通道是一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱。

image.png

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

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

image.png

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

下面通过一个例子来看一下 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,这些工具让我们可以方便地控制依赖库地版本。

image.png

1. GOPATH:

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

image.png

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

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

2. Go Vendor:

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

image.png

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。

软件测试

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

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

image.png

单元测试:

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/

项目实战 - 组件及技术点

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

项目的分层结构:

image.png

在实际的项目开发中,通常采用分层开发的方式,上图的分层模型就是我们常说的 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/gin.v1@v1.3.0
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mymel_晗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值