这次我们学习的是GO语言进阶的知识,这里分为三部分,先是语言进阶,接着是依赖管理,最后就是测试
语言进阶
goroutine
介绍
是一种轻量级的线程,创建的成本和开销都很小,堆栈只有几kb。在程序启动后,只有一个goroutine来调用main函数,这个我们称它为主goroutine,也就是主协程
创建协程
GO语言使用go关键字+函数/方法创建goroutine
package main
import (
"errors"
"fmt"
"time"
)
func HelloWorld() {
fmt.Println("hello world")
}
func main() {
go HelloWorld()
time.Sleep(1 * time.Second)
fmt.Println("我在后面")
}
先是通过go +函数创建了一个协程,但是创建协程之后,如果没有sleep,我们就看不到这个协程的输出,执行速度很快
channel
介绍
协程和协程之间可以通过两种方式进行交流,一种是通过通信来共享内存,一种是通过共享内存来通信,通过通信来共享内存,其实就是在协程之间构建了通道来交流信息,而通过共享内存来通信,就是建立一个临界区,然后各个协程一起在这里交流信息,提倡通过通信共享内存而不是,共享内存来通信,也就是通过通道来进行
创建通道
GO 语言通过 make(chan type size)type为元素类型,size为通道的缓冲大小可以为无
package main
func main() {
src := make(chan int) //无缓冲通道
dest := make(chan int, 3) //缓冲为3的通道
go func() {
defer close(src) //
for i := 0; i < 10; i++ {
src <- i
}
}() //协程一
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}() //协程二
for i := range dest {
println(i)
} //主协程
}
这里创建了两个协程,两个通道,协程一将数据输入给src通道,然后src通道连接着协程二,协程二通过src获取数据,由于src是无缓冲的通道,就直接获取,然后协程二将数据输入给dest通道,dest通道连接着主协程,主协程的速率并不与其他协程相匹配,所以dest通道就使用的是带有缓冲的通道,为了使主协程与协程二速率相匹配,协程二与协程一就是速率完全可以跟上,就使用的是无缓冲的通道,两个通道里的数据传输完就关闭通道。就类似于,协程一协程二都是生产空间的两个步骤,两个步骤的速率得一致,生产物品的速率才能快,而主协程就相当于消费者,它可以选择消费协程一和协程二一起生产出来的物品也可以不选择,为了使得这整个生产消费的过程高效,就得使得消费者的速率和生产空间的速率得到一种平衡或者说匹配
sync
sync标准库包提供了一些用于实现并发同步的类型。这些类型适用于各种不同内存顺序需求。对于这些特定的需求,这些类型使用起来比通道更加有效率,代码实现更简单
Mutex
一个Mutex值常称为一个互斥锁。内置两个方法Lock(),Unlock(),用来保护一份数据不会被多个使用者同时读取和修改。
package main
import (
"sync"
"time"
)
var (
x int64
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
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second) //?
println("WithoutLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
//在共享内存这种方式的并发过程中有概率产生并发安全问题
//通过对临界区的保护来避免这种问题
}
这里对比了加锁和不加锁之间的区别哦,先是构建协程哦,两个函数都是从1加到2000,两个协程都是执行五次,两个函数的区别就是有没有加锁,这里得出的结果,加锁的结果是10000与预计值一致,而不加锁的结果为8382(这个结果不是一定的,也有可能为10000,这个结果比较随机,不准确)与预计值不同,这里出现这种结果的原因是:在共享内存这种方式的并发过程中有概率产生并发安全问题。所以我们需要通过对临界区的保护来避免这种问题
waitGroup
每个sync.WaitGroup 内置一个计数器,初始默认值为零,这个类型下还有三个方法:Add(delta),Done(),Wait()
package main
import (
"fmt"
"sync"
)
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() //阻塞主程序直到当五个协程都完成
}
func hello(i int) {
println("hello goroutine :" + fmt.Sprint(i))
}
Add(delta)是用来改变计算器的默认值,改变其值为delta,Done()用来减少计数器的值,减一,Wait()用来阻塞主程序,直到计数器为零,这里就是将我们需要完成的协程数计入计数器中,然后每完成一个协程计数器就减一,直到减到0之后,这个主程序就不阻塞
依赖管理的几种方式
GoPATH
介绍及特点
GOPATH 是 Go 语言中使用的一个环境变量,GOPATH对应的那个路径是我们存放项目代码的路径,其实也就是我们写代码的地方。那个路径下会有三个文件夹分别为bin、pkg、src
-
bin:存放项目编译的可执行二进制文件
-
pkg:存放项目编译的中间缓存文件
-
src:存放项目源码,也就是其实我们就是在这个目录下写代码
项目代码直接依赖src下的源码
go get 下载最新版本的包到src目录下
设置GoPATH
首先我们得找到设置环境变量的地方,右击此电脑->点击属性->找到高级系统设置并点击->点击环境变量
然后就是设置环境变量,我们主要是得设置GOPATH和GOROOT以及Path,GOPATH就是我们写代码存放的路径,直接就在系统变量和用户变量处新建然后放入对应路径即可;GOROOT就是我们安装go工具的路径,与前面GOPATH同理;Path在我们系统变量和用户变量都有,直接将go工具bin目录的路径放进去即可,系统变量用户变量都要放。
最后设置完后,打开命令窗口,输入go env 在里面找到GOPATH和GOROOT,如果成功了,它们后面会对应有路径
弊端
了解了GoPath之后,我们知道我们必须就是得设置这个GoPath去确定我们的工作区也就是我们代码或者项目的存放地,而当我们有多个项目时,我们要么就全部都放在GoPath路径下的src目录下,这会导致目录下非常杂乱,不好管理。
但是针对多个项目,我们可以有其他方法,我们可以设置多个不同的GoPath对应多个不同的项目这样我们就可以非常方便的管理多个项目,而且每个项目下载包时会下载到该项目对应的GoPath路径下的src目录下,但是这里有一个问题就是,我们不同的项目可能会用到同一个依赖包,也就是说同一个依赖包,我们可能会下载多次,长久下来会导致有许多重复的包占用空间导致磁盘空间不够。
这说明我们不能两全,要么就特别杂乱不好管理,但是节省空间,要么就是牺牲空间,但是项目之间就分隔开。除了这些问题,运用GoPath引用其他项目时,不能处理版本不同的问题
Go Vender
改进
在GOPATH的基础上,在src文件下加入了vender文件,用来引入当前项目所依赖的包的副本,解决了就是多个项目需要同一个依赖包的冲突问题
弊端
无法控制依赖的版本
更新项目可能会出现依赖冲突,导致编译出错
根本就是没有真正把握版本的概念,依旧是依赖源码
Go Module
三要素
通过用go.mod文件来管理依赖包版本,描述其中依赖的包
通过go get/go mod指令工具来管理依赖包
通过用一个中心仓库Proxy来管理依赖库
go.mod 里对依赖的配置
我们一个单元可以有多个项目,然后项目会引用多个依赖,go.mod就能更好的管理这些依赖 配置情况:
先是需要管理的单元,放在第一行
接着是go原生库的版本号,我这里的版本号为1.19
最后就是具体的依赖单元,也就是require里的,就是我这里需要的依赖
module GOTest//这
go 1.19
require (
github.com/stretchr/testify v1.8.4
rsc.io/quote v1.5.2
)
require (
github.com/bytedance/gopkg v0.0.0-20230531144706-a12972768317
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/sampler v1.99.99 // indirect
)
依赖单元中,关键字indirect为间接依赖,关键字incompatible为可能存在不兼容
这里的每一个依赖后面都对应有其版本号,这里版本号分为两种,一种是语义版本,一种是基于commit的伪版本
-
语义版本:
${MAJOR}.${MINOR}.${PATCH}其中MAJOR就是我们俗称的大版本(各个大版本可以不兼容)
MINOR就是我们说的小版本(新增功能或者函数,与大版本必须兼容).
PATCH就是在当前版本做的代码修改,bug修复
例如:v1.3.0
-
基于commit的伪版本:
v0.0.0-yyyymmddhhmmss-abcdefgh1234其中v0.0.0为版本前缀,与语义版本是一样的。
yyyymmddhhmmss为时间戳,也就是提交commit的时间。
abcdefgh1234为十二位校验码
Proxy仓库的形成
-
形成的原因
从第三方获取依赖,无法保证构建的稳定性,无法保证依赖的可用性,会增加第三方平台的压力
构建Proxy服务站点,缓存原站中的软件内容,缓存的软件版本也不会改变,保证了其稳定性
-
配置方法
配置Proxy 配置GOproxy的环境变量GOPROXY=proxy1.cn,proxy2.cn,direct proxy1->proxy2->direct(源站),也就是当我们需要获取依赖时,会优先在proxy1.cn下查找是否有对应的依赖,如果没有再去proxy2.cn下查找,如果还是没有就会去direct也就是源站去获取并且缓存到前面两个站点里
go get 工具
go get 加路径 再加后缀可以实现一些功能
@update 显示默认依赖
@none 删除依赖
@v1.1.2 获取tag版本
@23dfdd5 获取特定的commit
@master 获取分支的最新commit
go mod 工具
go mod init 初始化,创建go.mod文件(项目的开始)
go mod download 下载go.mod文件中指明的所有依赖,使用此命令来下载指定的模块
go mod tidy 增加需要的依赖,删除不需要的依赖(项目最后再执行可删除不需要的依赖)
go mod graph 查看现在的依赖结构,生成项目所有依赖的报告
go mod vender 导出项目所有的依赖到vender目录,从mod中拷贝到项目的vender目录下
go mod verify 校验一个模块是否被篡改过,查询某个常见的模块出错是否已经被篡改
go mod why 查看为什么需要依赖某模块,查询某个不常见的模块是否是哪个模块的引用
测试
测试是避免事故的最后一道屏障
单元测试
介绍
单元测试是指软件中的最小可测试单元进行检查和验证。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试
过程
输入->测试单元->输出,最后输出和期望的校对,这里的测试单元包含函数和模块等
优点
保证质量,提升效率
规则
- 测试文件以_test.go结尾,注意这个一定是得一模一样,这个是区分大小写的,写成_Test.go的识别不了
- func TestXxx(*testing.T)测试函数的格式,按照这样写准没错,不要自己乱创新
- 初始化逻辑放到TestMain ,TestMain中可以装载数据,配置初始化等一些前置工作 然后运行整个程序,测试后,释放资源等收尾工作,其实就是测试的主要地方
- 在go mod的建立的基础下进行,创建文件要遵循一定的格式xxx_test文件
单元测试示例
先是得在go mod的环境下进行,所以得先配go mod,
-
在cmd命令窗口,输入
go env,然后查看GO111MODULE的值,如果没有配go mod则会是空,这里可以设置三个值分别为:on,auto,off,on就是打开这个模式,off就是关闭这个模式,auto就是根据当前目录是否有go.mod文件或者包含在这个文件下自动选择是否打开 -
如果没有配置的就同样在cmd命令窗口输入
go env -w GO111MODULE=on -
除了这个要打开,我们还有设置Proxy仓库,同样是在cmd命令窗口输
go env -w GOPROXY=https://goproxy.cn,direct
配完go mod之后,我们就可以新建一个项目,注意go mod不能和GOPATH共存,所以我们新建的项目不能在GOPATH对应的路径下,以vscode环境下为例,我们新建的项目一般是没有go.mod文件的,我们需要在终端转移到对应项目目录下输入:go mod init 项目名,这里的项目名也就是文件名,其实vscode里的终端不需要刻意转移,它默认就是你项目的目录下,这些操作完之后,你的文件夹下就会多出来一个go.mod文件
这个时候先不要急,我们还得更新依赖,项目运行所需要的最小依赖,所以还是在终端输入:go mod tidy
搞完以上这些我们就可以建项目了,这里以hello.go和hello_test.go为例
hello.go
package GOTest
import()
func helloTom() string {
return "jerry"
}
hello_test.go
package GOTest
import (
"testing"
)
func test_hello(t *testing.T) {
want := "tom"
got := helloTom()
if got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}
这里我们有两种运行方式,第一种就是在终端输入go test hello_test.go hello.go,注意这个输入的时候一定要是在这个测试文件的目录下,如果你的文件是在src目录下,要转移到src目录下,第二种就是以vscode为例,左边一列有一个测试按钮,点击就可以直接测试,或者在哪个测试函数旁边有一个绿色的三角形,也可以直接测试。这里测试的结果为:
--- FAIL: Test_hello (0.00s)
hello_test.go:11: Hello() = "jerry", want "tom"
FAIL
FAIL command-line-arguments 0.487s
FAIL
覆盖率
-
定义:测试函数中所实现功能部分的代码行数/整个测试函数的代码行数
-
如何测覆盖率:go test xxx_test.go xxx.go --cover
实际覆盖率:一般:50%-60% 较高80%
-
测试分支相互独立,全面覆盖,测试单元粒度足够小,函数的单一原则
-
测试用例:
待测试文件:passline.go
package GOTest
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
测试文件:passline_test.go
package GOTest
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass) //true是期望值,isPass是实际值
} //刚开始这个测试只测试了JudgePassLine()函数true的部分,也就是前两行代码,总共代码就三行,代码覆盖率为2/3
这里我们在终端输入:go test passline_test.go passline.go --cover,可以得出以下结果:
ok command-line-arguments 0.691s coverage: 66.7% of statements
我们可以看到这里的覆盖率就只有2/3,说明有些情况我们并没有考虑到,我们返回到原测试文件可以发现我们只测试了这个函数值为true的部分,也就是这个函数的前两行,最后一个false部分我们并没有做出测试,知道原因之后我们加上去可以得出:
package GOTest
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass) //true是期望值,isPass是实际值
} //刚开始这个测试只测试了JudgePassLine()函数true的部分,也就是前两行代码,总共代码就三行,代码覆盖率为2/3
func TestPassLineFalse(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
} //当测试完剩下的部分,覆盖率就为1
这个时候我们依旧在终端输入:go test passline_test.go passline.go --cover,可以得出以下结果:
ok command-line-arguments 0.068s coverage: 100.0% of statements
这个时候我们的覆盖率变成了百分之百,但是在实际的项目过程中几乎很难使覆盖率达到百分百,正常的话是50%到60%,较高的覆盖率就到80%,这种测试就是已经非常完善了
依赖
单元测试会引用大量外部依赖来实现幂等和稳定
- 幂等就是多次测试都是相同的结果
- 稳定就是任何函数在任何时间拿出来都可以单独运行
但是引用大量外部依赖这本身就是不稳定的,容易受到一些网络因素影响,推出用mock
Mock
开源包
github.com/bouk/monkey
原理
为函数/方法打桩,不再依赖本地的文件 其实就是创建一个虚拟的函数或者方法来执行原来的功能又不会依赖本地的文件
适用的场景
依赖模块没有开发完成,或者测试单元依赖的对象难以模拟或者构造比较复杂,又或是只想关注被测单元的的功能的逻辑,总之就是不想要依赖进来,将依赖隔离开去进行测试
基准测试
介绍
go提供了基准测试框架,基准测试是Golang中专门用于测试性能的一种测试方式。它通过反复执行同一段代码,统计执行时间、内存使用情况等指标,以此评估代码的性能表现。
作用
- 可以检测代码的性能问题,找出代码的瓶颈并进行优化。
- 可以验证优化后的代码的性能是否得到了提升
规则
- 测试文件以_test.go结尾
- func BenchmarkXxx(b *testing.B)测试函数的格式
- 函数体中,要使用for循环反复执行Function()函数
- 在go mod的建立的基础下进行,创建文件要遵循一定的格式xxx_test文件
测试用例:
待测试文件:jiZun.go
package GOTest
import (
"github.com/bytedance/gopkg/lang/fastrand"
)
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[fastrand.Intn(10)]
}
测试文件:jiZun_test.go
package GOTest
import (
"testing"
)
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
在终端输入go test -bench .,可以得出结果:
goos: windows
goarch: amd64
pkg: GOTest/src
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkSelect-16 172283589 6.901 ns/op
BenchmarkSelectParallel-16 24490645 43.56 ns/op
PASS
ok GOTest/src 3.078s
我们这里主要是看这两个函数的效率,也就是这里的第五行和第六行,第一个函数是串行的测试,第二个则是并行的测试,看效率还是得看这里的两个参数,一个是这里的172283589这么一大长串数字,它这里的含义就是这个逻辑循环的次数,还有一个是6.901 ns/op,这里的含义是每一个逻辑执行需要6.901纳秒,知道这两个参数之后发现这里的效率都不是很高,原因是rand在高并发实践中容易使得性能发生问题,这里可以去使用fastrand,这个更加的稳定,这个包需要导入github/bytedance/gopkg/lang/fastrand,更改后的代码如下:
package GOTest
import (
"github.com/bytedance/gopkg/lang/fastrand"
)
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[fastrand.Intn(10)]
}
继续在终端输入go test -bench .,得出的结果为:
goos: windows
goarch: amd64
pkg: GOTest/src
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkSelect-16 250256304 4.793 ns/op
BenchmarkSelectParallel-16 1000000000 0.5537 ns/op
PASS
ok GOTest/src 3.012s
这个时候我们再来看这个参数,你会发现这个速度完全更快了,逻辑执行的时间大大缩短,由原来的43.56 ns/op降为了 0.5537 ns/op,效率提高了很多
思考
这里我们会发现一个问题,我们这里在进行基准测试时,在终端输入的命令是针对该包下的所有的基准测试的文件,如果我们想要只测试某一个函数的性能,或者我们只测试某个子包里函数的性能,我们该使用什么命令呢,我这里整理了一下:
-
指定函数/方法,可以用正则表达式来指定方法名
所有以Select结尾的方法:
go test -bench='Select$' pkg,这里的pkg是这个函数所在的包名,如果是已经是在当前目录下了就可以省略所有以BenchmarkSelect开始的方法:
go test -bench='^BenchmaekSelect pkg',这里的pkg是这个函数所在的包名,如果是已经是在当前目录下了就可以省略 -
指定子包:
go test -bench pkg/xxx,pkg下的xxx包
总结和心得
-
总结:这次的课程学习了GO语言进阶,第一个学习的就是go语言的并发工作,里面涉及了是选择在通信中共享内存还是在共享内存中通信,我们学过之后肯定是选择在通信中共享内存,这其中还涉及到goroutine,还有channel等知识,都是非常重要的,在项目中也是经常使用的,得掌握;第二个就是依赖管理,其实就是对go项目相关文件的管理, 我们如果是个人项目,GOPATH可以满足需求,但是如果涉及多个项目,建议还是用Go Module,得了解其中演变的过程,和各自的优势,然后得学会使用go mod,这个很重要;最后一个就是测试了,测试也是非常重要的,在项目中测试就是给项目把关的,项目能不能行就得看测试能不能通过,项目能不能更加高效,也得通过测试来获取信息,然后进一步优化整个项目
-
心得和建议:学这部分知识应该是我们写项目比较基本的知识,可能前面还在学基础语法,现在就过渡到项目上来了,有些知识都没有接触过,特别是对于没有项目经验的来说,其实在其他语言上对并发编程,对依赖管理以及测试方面都有不同的处理,但是这都是一个项目应该要有的,无论是以何种语言来写,这部分内容我觉得讲的不是很全面,很多知识都理解很片面,更多的内容是需要我们自己去探索的,然后就是这种只是偏理论型,实操一个开源项目,可能会有更深的感受