Go语言进阶 | 青训营笔记

205 阅读4分钟

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

前言

本篇文章内容:

  • Go语言高性能的本质
  • Go语言依赖管理的演进路线
  • 单元测试实践
  • 最后的项目练习

一、Go语言的高性能

并发 VS 并行

并行:多线程程序在一个核的cpu上运行。

并行:多线程程序在多个核的cpu上运行。

01.Goroutine

线程:用户态、轻量级线程,栈 MB 级别。

协程:内核态、线程跑多个协程,栈 KB 级别。

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)
}

02.CSP

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

03.Channel

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

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

04.并发安全 Lock

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
    }
}

05.WaitGroup

计数器: 开启协程+1;执行结束-1;主协程阻塞直到计数器为0.

func ManyGoWait() {
    var wg sync.WaitGroup
    wq.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer wg.Done()
            hello(j)
        }(i)
    } 
    wg.Wait()
}

首先通过add方法,对计数器+5,然后开启协程,每个协程执行完后,通过done对计数器减少1,最后wait主协程阻塞,计数器为0退出主协程,右边是最终的输出结果

二、依赖管理

对于实际工程不可能基于标准库0~1编码搭建,而其他涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要。

而Go的依赖管理主要经历3各阶段:GOPATH —> Go Vendor —> Go Module

GOPATH

GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区。

目录结构分为:

  • src:存放Go项目的源码;
  • pkg:存放编译的中间产物,加快编译速度;
  • bin:存放Go项目编译生成的二进制文件。

GOPATH的弊端:依赖于某一版本,无法实现多版本控制。

Go Vendor

  • 项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor

  • 依赖寻址方式:vendor => GOPATH

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package依赖的冲突问题。

Go Vendor的弊端:无法控制依赖的版本,更新项目又可出现依赖冲突。

Go Module

Go Modules是Go语言官方推出的依赖管理系统。

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

依赖管理三要素

1.配置文件,描述依赖 go.mod

2.中心仓库管理依赖库 Proxy

3.本地工具 go get/mod

依赖配置 - go.mod

Go.mos.png 每个单元依赖用模块路径+版本来唯一标示

依赖配置 - version

语义化版本 MAJOR.{MAJOR}.{MINOR}.${PATCH} v1.3.0 v2.3.0

依赖配置-incompatible

incompatible.png

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

依赖分发-回源

  • 无法保证构建稳定性:增加/修改/删除软件版本
  • 无法保证依赖可用性:删除软件
  • 增加第三方压力:代码托管平台负载问题

依赖分发-Proxy

Go Proxy是一个服务站点,实现了供“immutability”和“available”的依赖分发;使用 Go Proxy 之后,构建时直接从 Go Proxy 站点拉取依赖。

依赖分发-变量 GOPROXY

GOPROXY是一个 Go Proxy 站点URL列表,可以使用“direct”表示源站。

工具-go get

go get example.org/pkg

  • @update 默认
  • @none 删除依赖
  • @v1.1.2 tag版本,语义版本
  • @23dfdd5 特定的commit
  • @master 分支的最新commit

go mod

  • init 初始化,创建go.mod文件
  • download 下载模块到本地缓存
  • tidy 增加需要的依赖,删除不需要的依赖。

三、测试

单元测试主要包括,输入、测试单元、输出、以及校对、单元的概念比较广,包括接口、函数、模块等;用最后的校对来保证代码的功能与我们的预期相符;单侧一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性,另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。

单元测试-规则

  • 所有测试文件以 _test.go 结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到 TestMain 中

单元测试-例子

func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    if output ≠ expectOutput {
        t.Errorf("Expected %s do not match actual %s", expectOutput,output)
    }
}

单元测试-Tips

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

单元测试-依赖

外部依赖 => 稳定&幂等

稳定是指相互隔离、能在任何时间、任何环境、运行测试。

幂等是指每一次测试运行都应该产生与之前一样的结果。

单元测试-Mock

monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock。

快速Mock函数

//Patch将函数替换为另一个函数
func Patch(target, replacement interface{}) *PatchGuard {
   t := reflect.ValueOf(target)
   r := reflect.ValueOf(replacement)
   patchValue(t, r)

   return &PatchGuard{t, r}
}

func Unpatch(target interface{}) bool {
   return UnpatchValue(reflect.ValueOf(target))
}

基准测试

基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。在实际项目开发中,会遇到代码性瓶颈才会使用。

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供了基准测试的能力

四、项目实战

青训营话题页forum.juejin.cn/youthcamp/p…

需求:社区话题页面

  1. 实现一个展示话题(标题,文字描述)和回帖列表后端http接口
  2. 本地文件存储数据(仅实现一个本地web服务即可)。
  3. 话题和回帖数据用文件存储,本地id生成需要保证不重复、唯一性。

组件及技术点

引用:

该文章部分内容来自以下课程:

感谢小伙伴的阅读,欢迎一起学习和讨论!