高并发
package main
import (
"fmt"
"time"
)
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)
}
func main() {
HelloGoRoutine()
}
//go语言在调用函数前加入go关键字,就可以开启一个新的goroutine协程执行该函数
//主协程一结束默认终止协程,可以通过time.Sleep()来控制主线程的结束时间,但如此操作过于简陋,所以一般不使用
//这里还有新的语法点,在函数定义后加入括号,表示立即执行该匿名函数
Channel
Channel可以看作是Go语言中用于通信的消息队列,分为无缓冲通道和有缓冲通道两种
前者也被称为同步通道,通道的容量代表着通道中能存放多少元素。
后者能够解决生产者和消费者执行通道内容速度不匹配的问题,
创建格式:make ( chan 元素类型,[ 缓冲大小 ] )
package main
func CalSquare() {
src := make(chan int) //无缓冲通道
dest := make(chan int, 3) //有缓冲通道
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}() //生产者,发送0~9数字到src通道中
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}() //消费者,从src通道中取值,计算平方后发送到dest通道中
for i := range dest {
println(i)
}//主goroutine从dest通道中取值并打印
}
func main() {
CalSquare()
}
WaitGroup
内部维护了一个计数器,可以增加(开启协程)或者减少(协程结束),可以使主协程阻塞直到计数器为 0 。即当所有协程任务执行完毕后, 主协程再执行完毕,避免提前关闭主协程导致子协程未完全执行。
创建格式:var 变量名称 sync.WaitGroup
.Add(n) :增加n个协程单位
.Done() :减去一个协程单位
.Wait() :阻塞主协程,直到计数器的协程数量清零
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello goroutine: " + fmt.Sprint(i))
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)//5个协程,计数器增加5
for i := 0; i < 5; i++{
go func(j int){
defer wg.Done()//协程执行完毕,计数器减1
hello(j)
}(i)
}
wg.Wait()//阻塞主协程,直到计数器为0
}
func main() {
ManyGoWait()
}
Mutex
是一个互斥锁,在主流编程语言的多线程操作中都支持使用互斥锁。Go语言也保留了这种操作,但其并不提倡使用内存共享来实现通信,所以在Go开发中少用。
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 Add() {
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)
}
func main() {
Add()
}
依赖管理
Go的依赖管理演进
GOPATH → Go Vendor → Go Module
不同环境(项目)依赖的版本不同,依赖管理的目的是控制依赖库的版本
GOPATH
GOPATH是Go语言支持的的环境变量,是Go项目的工作区,主要文件结构如下
bin:项目编译的二进制文件
pkg:项目编译的中间产物,加速编译
src:项目源码
项目代码直接依赖src下的代码,go get下载最新版本的包到src目录下
多个项目依赖于某一package的不同版本,会出现问题,因为GOPATH无法实现package的多版本控制
Go Vendor
在项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
依赖寻址方式 vendor ⇒ GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
在多依赖且依赖package相同但依赖版本不同的情况下,会造成依赖冲突,相同package的不同版本在此情况下不兼容
Go Module
是Go官方推出的依赖管理系统,解决了各种依赖冲突问题
通过 go.mod 文件管理依赖包版本
通过 go get/go mod 指令工具对依赖包进行管理
依赖管理的三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
依赖配置 - go.mod
module example/project/app //依赖管理基本单元
go 1.16 //原生库
require ( //单元依赖
example/lib1 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.mod文件并且主版本为2+的依赖
)
依赖标识:[Module Path][Version/Pseudo-version]
依赖配置 - version
语义化版本
{MINOR}.${PATCH}
V1.3.0
V2.3.0
基于 commit 伪版本
vx.0.0-yyyymmddhhmmss-abcdefgh1234
v0.1.0-20190725025543-5a5fe074e612 v0.0.0-20180306012644-bacd9c7ef1dd
依赖选择
当多个项目(A、B)依赖于同个项目(C)的不同版本(V1.3、V1.4),而又有一个项目依赖于上述项目(A、B),则在编译时会优先选择最低的兼容版本(V1.4)
依赖分发 - 回源
若直接使用版本管理仓库下载依赖会有以下缺陷
- 无法保证构建的稳定性(增加/修改/删除软件版本)
- 无法保证依赖可用性(删除软件)
- 增加第三方压力(代码托管平台负载压力)
依赖分发 - Proxy
Proxy是一个服务站点,会缓存源站中的软件内容和版本
用Proxy可以实现稳定和可靠的依赖分发
Proxy是十分强大的依赖分发系统,可以多层使用提高稳定性和可靠性,在实战项目中有重要的作用和意义
依赖分发 - 变量 GOPROXY
配置格式:GOPROXY=”proxy1.cn,proxy2.cm,direct”
服务站点URL列表,用逗号分隔,“direct”表示源站
Go Module就是依据该列表配置实现对Proxy的管理
项目查找依赖时优先从给定列表的最左边的URL尝试获取依赖
工具 - go get
用法:go get example.org/pkg
参数:
@update:默认
@none:删除依赖
@v1.1.2:tag版本,语义版本
@23dfdd5:特定的commit
@master:分支的最新版本
工具 - go mod
用法:go mod
参数:
@init:初始化,创建go.mod文件
@download:下载模块到本地缓存
@tidy:增加需要的依赖,删除不需要的依赖
测试
质量就是“生命”,测试是避免事故的最后一道“屏障”
回归测试 → 集成测试 → 单元测试,越往后成本越低,但覆盖率越大
单元测试
单元测试流程
输入 → 测试单元 → 输出 → 校对结果
测试单元:函数、模块等具有输入输出能力的代码区块都可以作为测试单元
单元测试的作用
- 保证质量
- 提升效率
单元测试 - 规则
- 所有测试文件以 _test.go 结尾,便于识别和区分
- 测试函数命名规范 *func TestXxx(t testing.T) ,符合规范才能运行测试函数
- 初始化逻辑放到 **func TestMain(m testing.M) *中
//Hello.go
package main
func HelloTom() string {
return "Jerry"
}
//Hello_test.go
package main
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s.", expectOutput, output)
}
}
单元测试 - assert(断言)
//Assert.go
package main
func HelloTom() string{
return "Tom"
}
//Assert_test.go
package main
import (
"github.com/stretchr/testify/assert"//导入第三方校对包assert,没有的话要用go get下载
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)//使用第三方包的方法校对结果
}
单元测试 - 覆盖率
衡量代码是否经过了足够的测试,对项目的测试水准进行量化,评估项目是否达到高水准测试等级
Go可以用 go test 测试文件.go 源文件.go —cover 进行覆盖率测试
//Judgement.go
package main
// JudgePassLine 判断分数是否及格
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
//Judgement_test.go
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)//对>=60的分支进行测试
}
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)//对<60的分支进行测试
}
//此时覆盖率为100%
单元测试 - Tips
实际项目中较难做到100%覆盖率 一般覆盖率:50%~60%,较高覆盖率80%+
提高覆盖率的方法:
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一原则
Mock测试
在项目中,单元可能依赖于文件、数据库、缓存等
外部依赖 ⇒ 稳定&幂等
幂等:重复运行一个测试案例,结果是一致的
稳定:单元测试相互隔离,可以实现在任意时间任何位置对任何函数进行测试
Mock机制,为函数或者方法打桩,替换原函数,使得函数不依赖本地文件测试
//ReadFile.go
package main
import (
"bufio"
"os"
"strings"
)
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
////ReadFile_test.go
package main
import (
"github.com/stretchr/testify/assert"
"bou.ke/monkey"
"testing"
)
func TestProcessFirstLine(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})//生成打桩函数
defer monkey.Unpatch(ReadFirstLine)//卸载打桩函数
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
基准测试
测试一段程序运行时的性能和测算CPU的损耗,Go内置了测试框架进行基准测试,使用方法类似单元测试。
- 测试函数命名规范 *func BenchmarkXxx(b testing.B)
- windows环境下 cmd输入 go test -bench=".” 进行压力测试
//Benchmark.go
package main
import (
"github.com/bytedance/gopkg/lang/fastrand"
"math/rand"
)
var ServerIndex [10]int
func InitServerIndex() {
for i := 0;i < 10;i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
func FastSelect() int {
return ServerIndex[fastrand.Intn(10)]
}
//Benchmark_test.go
package main
import "testing"
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()//串行基准测试,rand
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()//并行基准测试,fastrand
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
FastSelect()
}
})
}