这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
语言进阶
并发
go可以充分利用多核优势,高效运行
线程:有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
协程:协程(Coroutines)是一种比线程更加轻量级的存在。协程完全由程序所控制(在用户态执行),带来的好处是性能大幅度的提升。 一个线程内的多个协程的运行是串行的,这点和多进程(多线程)在多核CPU上执行时是不同的。当线程内的某一个协程运行时,其它协程必须挂起。
go
go语言中开启协程是比较简单的,在函数前使用go关键字
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) // 传递参数为i
}
time.Sleep(time.Second)
}
channel管道
go语言提倡通过通信来共享内存,而不是通过共享内存而实现通信
我们使用管道来进行通信
make(chan 元素类型, [缓冲大小])
根据有无缓冲通道,分为两种channel
为什么需要channel
- 主线程在等待所有goroutine全部完成的时间很难确定
- 手动等待时间过长或果断都会不利于程序的运行
- 通过加锁实现通讯不利于多个协程对全局变量的读写操作
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3) // 有缓冲区
go func() { // 生产数据
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() { // 计算
defer close(dest)
for i := range src { // 通过src使得该协程与上面的协程进行了通信
dest <- i * i
}
}()
for i := range dest { // 通过dest使得该协程与上面的协程进行了通信
//复杂操作,消费
// 这里采用有缓冲的chan是因为有的时候生产是速度要比消费的要快
println(i)
}
}
channel是线程安全的,无须加锁
在Go语言中,通道是
goroutine与另一个goroutine通信的媒介,并且这种通信是无锁的。换句话说,通道是一种允许一个goroutine将数据发送到另一个goroutine的技术。默认情况下,通道是双向的,这意味着goroutine可以通过同一通道发送或接收数据,如下图所示:
语言中,除了
chan string这样的写法能够使用读写功能双向管道外,还可以创建出单向管道,如<-chan string只能从管道中读取数据,而chan<- string只能够向管道中写入数据。作者:CV大使 链接:juejin.cn/post/699313… 来源:稀土掘金
Mutex
go也是有加锁的sync.Mutex
也可以通过共享内存实现通信
如果不加锁,多个协程并发执行,可能会同时操作一块内存的情况,也可能会有数据竞态
waitgroup
我们不知道协程的运行时间,go语言采用WaitGroup来实现计数器
有三个方法
- add(delta):计时器加delta
- done:计时器-1
- wait:阻塞直到计数器为0
当计时器为0代表所有的并发任务已经完成
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)
}
依赖管理
go module
测试
大致分为回归测试、集成测试、单元测试
单元测试
一定程度上决定了代码质量
规则
-
所有的测试文件以
_test.go结尾 -
func TestXxxx(*testing.T) -
初始化逻辑放到
TestMain中-
func TestMain(m *testing.M) { // 测试前:数据装载,配置初始化等前置工作 code := m.Run() // 跑包下的所有单元测试 // 测试结束,释放资源等收尾工作 os.Exit(code) }
-
例子
hello.go
package main
func HelloTom() string {
return "Jerry"
}
hello_test.go
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
except := "Tom"
assert.Equal(t, except, output)
}
返回
=== RUN TestHelloTom
hello_test.go:11:
Error Trace: hello_test.go:11
Error: Not equal:
expected: "Tom"
actual : "Jerry"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Tom
+Jerry
Test: TestHelloTom
--- FAIL: TestHelloTom (0.00s)
预期:Tom
实际:Jerry
<点击以查看差异>
FAIL
进程 已完成,退出代码为 1
如果正确返回
=== RUN TestHelloTom
--- PASS: TestHelloTom (0.00s)
PASS
进程 已完成,退出代码为 0
覆盖率
用于衡量代码是否经过了足够的测试;评价项目的测试水准;评估项目是否打到了高水准的测试等级
发现所有行的代码都被运行过了
依赖
幂等:在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同
mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。比如说你需要调用B服务,可是B服务还没有开发完成,那么你就可以将调用B服务的那部分给Mock掉,并编写你想要的返回结果。
原本的ReadFirstLine函数需要依赖文件,而Process函数依赖ReadFirstLine,但是由于文件可能是不确定的,我们可以通过打桩函数阿里对ReadFirstLine来返回一个固定的值,以构造一个答案,或者他也可能是没完成的函数,这样ReadFirstLine就相当于读到了line110了
基准测试
优化代码,内置的测试框架提供了基准测试的能力
同样的文件以_test.go结尾
函数用BenchmarkXxxx
例如我们要测试随机选择服务器Select的性能
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)]
}
测试代码
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer() // 这里是定时器重置,目的时去掉init的消耗时间
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()
}
})
}
参考:
www.liaoxuefeng.com/wiki/101695…
\