Go语言进阶 | 青训营笔记

105 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第2天

Go语言进阶

并发编程

线程 协程

线程属于内核态,创建,切换,停止都属于很重的系统操作,比较消耗资源。栈MB级别,在当今互联网高并发场景下,为每个任务创建一个线程是不现实的,因为会消耗大量的内存

协程属于用户态,轻量级线程,线程可以并发的跑多个协程,协程创建和调度由Go语言去完成,栈KB级别,Go语言一次可以创建上万个协程,这就是Go语言更适合高并发场景的原因所在

  • 如何开启协程

    Gorountine是Go语言程序并发的执行体

    要开启Gorountine只需要在调用函数的时候,在函数前面加上go关键字即可

    go 函数名( 参数列表 )
    

协程间通信 通道(Channel)

Go提倡通过通信共享内存而不是共享内存而实现通信

项目中应该避免对共享内存进行非并发安全的读写操作

Go其实也保留着通过共享内存来实现通信的机制

Channel把Gorountine做了个连接,就像是传输队列,遵循着先入先出,能保证收发数据的顺序,是可以让一个Gorountine发送特定的值到另一个Gorountine的一个通信机制

  • Channel

    Channel的创建需要通过make关键字

    make(chan 元素类型[缓冲区大小])
    

    根据缓冲区大小,又可分为

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

Sync

  • 并发安全 Lock

    lock sync.Mutex
    lock.Lock() //加锁,获取临界区资源
    lock.Unlock() //释放临界区资源
    
  • WaitGroup

    可以实现并发任务的同步,sync包下

    内部维护了一个计数器,计数器的值可以增加和减少

    三个方法:

    1. Add(delta int):计数器+delta
    2. Done():计数器-1
    3. Wait():阻塞直到计数器为0

依赖管理

Go依赖演进

GOPATH

Go Vendor

Go Module(广泛应用)

演进目的:

  • 不同环境(项目)依赖的版本不同
  • 控制依赖库的版本
  1. GOPATH

    • 环境变量 $GOPATH

      项目的工作区

      • bin:项目编译的二进制文件
      • pkg:项目编译的中间产物,加速编译
      • src:项目源码
    • 项目代码直接依赖src下的代码

    • go get 下载最新版本的包到src目录

    • 弊端:

      • 无法实现package的多版本控制
  1. Go Vendor

    • 项目目录下增加vendor

      vendor下存在当前项目依赖的副本

    • 依赖寻址方式:

      vendor => GOPATH

    • 弊端

      • 无法控制依赖的版本
      • 更新项目又可能出现以来冲突,导致编译出错
  1. Go Module

    • 通过go.mod文件管理依赖包版本
    • 通过go get/go mod指令工具管理依赖包
    • 终极目标:

      定义版本规则和管理项目依赖关系

依赖管理三要素

  1. 配置文件,描述依赖:go.mod
  2. 中心仓库管理依赖库:Proxy
  1. 本地工具:go get/mod

依赖配置

1. go.mod

由三部分构成

  1. 模块路径

  2. Go原生库的版本号

  3. 单元依赖

    依赖标识:[Module Path][Version/Pseudo-version]

2. version

GOPATH和Go Vendor都是源码副本方式进行依赖,没有版本规则的概念

Go Module为了方便做版本管理,定义了自己的版本规则

  • 语义化版本

    ${MAJOR}.${MINOR}.${PATCH}

    MAJOR:大版本,不同的MAJOR版本是可以不兼容的,不同的MAJOR间是代码隔离的

    MINOR:新增函数或者功能,需要保持在MINOR下保持前后兼容

    PATCH:做代码BUG修复

    V1.3.0

    V2.3.0


  • 基于 commit 伪版本

    vx.0.0-yyyymmddhhmmss-abcdefgh1234

    版本前缀:和语义化版本是一样的

    时间戳:提交的时间戳

    十二位哈希码前缀

    提交后,Go默认生成

3. indirect

在go.mod中,对于没有直接依赖的依赖模块就会标记为非直接依赖,用indirect标识出来

4. incompatible

在go.mod中

  • 主版本2+模块会在模块路径增加/vN后缀
  • 对于没有go.mod文件并且有主版本2+的依赖,会加上+incompatible后缀

依赖分发

1. 回源

Github、SVN...

  • 无法保证构建稳定性

    增加/修改/删除软件版本

  • 无法保证依赖可用性

    删除软件

  • 增加第三方压力

    代码托管平台负载问题

2. Proxy

一个服务站点,缓存原站中的软件内容,缓存的版本不会改变

保证依赖的稳定性,实现稳定可靠的依赖分发

  • 变量 GOPROXY

    通过GOPROXY环境变量来配置Proxy

    服务站点URL列表,用,分割

    最后direct表示源站,如果前面那些站点都没有依赖的话,会回源到第三方代码平台上去

工具

1. go get

go get example.org/pkg:拉取MAJOR版本最新commit

@update:等于不加

@none:删除依赖

@v1.1.2: tag版本,语义版本

@23dfdd5: 特定的commit

@master: 分支的最新commit

2. go mod

init:初始化项目的时候,需要用init创建go.mod文件

download:下载模块到本地缓存,把所有的依赖拉下来

tidy:增加需要的依赖,删除不需要的依赖

测试

单元测试

1.规则
  • 所有测试文件以_test.go结尾
  • 函数命名func TestXxx(t *testing.T)
  • 初始化逻辑放到func TestMain(m *testing.M)

    测试前:数据装载、配置初始化等前置工作

    code := m.Run()

    测试后:释放资源等收尾工作

    go test [flags][packages]

2. assert

github.com/stretchr/testify/assert

很多开源的assert包可以帮助实现equal或not equal的问题

3. 覆盖率

代码覆盖率可以用来评估单元测试

加上--cover参数

  • 一般覆盖率:50%~60%,较高覆盖率:80%+
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责
4. Mock

单元测试有两个目标:稳定&幂等

幂等:重复运行一个测试的case的时候,结果是一样的

稳定:单元测试是能相互隔离的,能在任何时间,任何函数运行

为了实现目的,单元测试中需要使用Mock机制,使得测试可以在任何时间,任何环境执行

Mock组件monkey:github.com/bouk/monkey

Patch(原函数(目标被替换的函数), 需要打桩的函数):打桩函数

Unpatch(原函数):测试结束后,需要把桩卸载掉

基准测试

测试一段程序运行时的性能和CPU的损耗

  • 函数命名BenchmarkXxx(b *testing.B)
  • b.ResetTimer:重置定时器
  • 基准测试的并行通过b.RunParallel来实现
  • 优化随机函数性能问题:fastrand,rand在高并发场景会造成一定性能问题

实践

在实践出现连接失败的问题:

connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.

只要配置一下代理就可以解决了

go env -w GO111MODULE=on

go env -w GOPROXY=https://goproxy.cn,direct