1.语言进阶
并发编程
并发:多线程程序在一个的核cpu上运行。
并行:多线程程序在多个核的cpu上运行。
Go可以充分发挥多核优势,高效运行。
线程:系统中比较昂贵的系统资源,属于内核态 栈MB级别。
协程:属于轻量级的线程,属于用户态,栈KB级别。
创建协程的函数:
go func(){
}()
例子:
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提倡通过通信共享内存,而不是通过共享内存实现通信。
共享内存实现通信:
优点:提高程序运行效率。
缺点:会导致数据不一致、数据竞态等问题。
通过通信共享内存:
优点:channel通道让协程进行连接,保证收发顺序,不会有数据不一致等问题。
Channel
channel通过make关键字创建 可分为有缓冲通道 和 无缓冲通道
区别:
无缓冲被称为同步通道。
有缓冲通道会阻塞发送,直到有人取走缓冲内的数据。
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 {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
}
func main() {
CalSquare()
}
并发安全Lock
暴力加锁:
import (
"sync"
"time")
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 10000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 10000; i++ {
x += 1
}
}
func add() {
x = 0
for i := 0; i < 10; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock", x)
x = 0
for i := 0; i < 10; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithOutLock", x)
}
func main() {
add()
}
通过WaitGroup实现协程的同步阻塞:
因为已知5个协程,所以add对计数器+5,每个协程执行完毕后,通过done对计数器减少1,最后wait阻塞主协程,计数器为0退出主协程
func HelloGoRoutine() {
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()
}
2.依赖管理
实际项目工程不会基于标准库搭建,会更多的关注业务逻辑的实现,所以其他涉及框架、日志、driver和collection等一系列依赖都会通过sdk的方式引入。
依赖管理的三个阶段:
GOPATH -> GO Vendor -> Go Module
GOPATH
环境变量$GOPATH:
1.项目代码直接依赖src下的代码
2.go get下载最新版本的包到src的目录下
弊端:
无法实现package的多版本控制
Go Vendor
在项目目录下增加了vendor文件,所有依赖包以副本形式放在$ProjectRoot/vendor。
依赖寻址方式:vendor => GOPATH。
解决了多个项目需要同一个package依赖的冲突问题。
弊端: 可能出现依赖冲突,导致编译错误。
Go Module
go语言官方推出的依赖管理系统
依赖管理三要素:
1.配置文件,描述依赖了哪些包和包的定位 go.mod
2.中心仓库管理依赖库 Proxy
3.本地工具 go get/mod
间接依赖在配置文件中用indirect标识
Go Modules系统中定义的依赖,可以对应到多版本代码管理系统中某一项目的特定提交或版本,所以对go.mod中定义的依赖可以直接从对应仓库中下载指定软件依赖。
但直接使用版本管理仓库下载依赖有3个问题:1.无法保证构建稳定性 :作者增加/删除/修改 软件版本
2.无法保证依赖可用性 :依赖软件作者可以直接删除软件,导致依赖不可用
3.大幅增加第三方代码托管平台的压力
所以
Go Proxy诞生了
Go Proxy是一个服务站点,他会缓源站中的软件内容,缓存的版本不会改变,而且在源站软件删除后依然可用。
GOPROXY="proxy1.cn,https://proxy2.cn,…"
变量GOPROXY中,依赖的寻址路径会优先从proxy1下载,如果proxy1中不存在,会在proxy2中寻找,如果proxy2中不存在,则会直接回到源站下载依赖。
go mod init 初始化,创建go.mod文件
go mod tidy 增加需要的依赖,删除不需要的依赖 (常用)
3.测试
测试
测试是避免事故的最后一道屏障。
测试包含了以下三个:
回归测试
集成测试
单元测试
从上到下,覆盖率逐渐变大,但成本却逐渐降低。
单元测试-规则:
1.所有测试文件以_test.go结尾
2.测试函数命名规范:
func TesXxx(*testting.T)
3.初始化逻辑放到TestMain中
例子:
func TestHelloTom(t *testing.T) {
output := HelloTom()
expect := "tom"
if output != expect {
t.Errorf("Expect is %s do not match actual %s", expect, output)
}
}
代码覆盖率测试
例子:
func JudgePassLine(score int64) bool {
if score >= 60 {
return true
}
return false
}
import (
"github.com/stretchr/testify/assert"
"testing")
func TestJudgePassLineTrue(t *testing.T) {
ispass := JudgePassLine(70)
assert.Equal(t, true, ispass)
}
func TestJudgePassLineFalse(t *testing.T) {
notpass := JudgePassLine(40)
assert.Equal(t, false, notpass)
}
一般覆盖率:50%~60%,较高覆盖率80%+,覆盖率越高成本越大。
测试分支相互独立,全面覆盖。
测试单元力度足够小,函数单一职责。
mock测试
对函数进行mock,屏蔽对于文件的依赖。(个人理解和回调函数差不多?)
基准测试
基准测试以Benchmark开头,入参是testing.B,用b中的N值反复递增循环测试。