Go语言上手-工程实践 | 青训营笔记

218 阅读2分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

1 语言进阶

1.1 并行与并发

Snipaste_2022-05-08_16-09-48.jpg 并行是多线程程序同一时刻在多个核上运行
并发是在一个时间间隔内多个线程交替运行

1.2 goroutine

Snipaste_2022-05-08_15-21-15.jpg goroutine简单实列

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启动协程
运行结果

Snipaste_2022-05-08_12-13-40.jpg

1.3 CSP(Communicating Sequential Procedure)

Snipaste_2022-05-08_16-10-42.jpg

1.4 Channel

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

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int,2) channel具体使用
package concurrence

//src 子协程发送0-9数字
//dest 子协程计算数字的平方
//主协程输出

//并发安全的
func CalSquare() {
   src := make(chan int)     //无缓冲通道
   dest := make(chan int, 3) //有3个缓冲通道
   go func() {               //go开启src子协程
      defer close(src) //延迟关闭
      for i := 0; i < 10; i++ {
         src <- i //将数字送入src通道 生产者
      }
   }()
   go func() { //go开启dest子协程
      defer close(dest)
      for i := range src {
         dest <- i * i //dest接收并计算,消费者 消费者消费慢设计为有缓冲通道
      }
   }()
   for i := range dest { //go开启主协程
      println(i)
   }
}

1.5并发安全Lock

var lock sync.Mutex 定义锁
在对共享变量操作前先加锁 lock.Lock()
操作完毕解锁lock.Unlock()

1.6 WaitGroup

三个主要方法

Add(delta int) 计数器+delta
Done() 计数器减1
Wait() 阻塞直到计数器为0
开启协程 计数器+1 ; 执行结束 计数器-1;主协程阻塞到计数器为0
使用实例

package main

import (
   "fmt"
   "sync"
)

func hello(i int) {
   println("hello goroutine: " + fmt.Sprint(i))

}
func HelloGoRoutine() {
   var wg sync.WaitGroup //定义WaitGroup实例
   wg.Add(5)//计数器定为5
   for i := 0; i < 5; i++ {
      go func(j int) {
         defer wg.Done()//计数器减1
         hello(j)
      }(i)
   }
   wg.Wait()//阻塞
}
func main() {
   HelloGoRoutine()

}

2 依赖管理

2.1 Go依赖管理演进

Snipaste_2022-05-08_16-12-41.jpg

2.1.1 GOPath

  1. 设置GOPATH环境变量

Snipaste_2022-05-08_16-16-56.jpg 2. GOPATH 目录下有三个文件夹

  • bin 项目编译的二进制文件
  • pkg 项目编译的中间产物,加速编译
  • src 项目源码
    项目代码直接依赖src下的代码
    go get 下载最新版的包到src目录下
  1. 缺点

Snipaste_2022-05-08_16-22-30.jpg A和B依赖于某一package的不同版本
无法实现package的多版本控制

2.1.2 Go Vendor

  1. 在项目目录下增加vendor文件,所有依赖包副本形式放在¥PeojectRott/vendor目录下
  2. 依赖寻址方式:vendor =》 GOPATH 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
    3.缺点

Snipaste_2022-05-08_16-28-12.jpg

  • 无法控制依赖的版本
  • 更新项目可能出现依赖冲突,导致编译错误

2.1.3 Go Module

  • 终极目标:定义版本规则和管理项目依赖关系
  • 通过go.mod 文件管理依赖包版本
  • 通过go get / go mod 指令工具管理依赖包

2.2 依赖管理三要素

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

2.2.1 依赖配置-文件go.mod

Snipaste_2022-05-08_16-36-28.jpg

2.2.2 依赖配置-version

  • MAJOR: 大版本
  • MINOR: 新增函数
  • PATCH: 修复 Snipaste_2022-05-08_16-41-09.jpg

版本前缀-时间戳-12位哈希码 Snipaste_2022-05-08_16-45-29.jpg

2.2.3 依赖配置-indirect

直接依赖和间接依赖

Snipaste_2022-05-08_16-47-15.jpg

2.2.3 依赖配置-incompatible

Snipaste_2022-05-08_16-48-46.jpg

2.2.4 依赖配置-依赖图

Snipaste_2022-05-08_16-52-02.jpg 正确答案为B
应该选择兼容的最低版本

2.2.5 依赖分发-回源

Snipaste_2022-05-08_16-55-59.jpg go module的依赖分发,解决的是从哪里下载,如何下载的问题
github是常见的代码托管系统平台,go module 中定义的依赖,最终可以定义到多版本代码管理系统中某一项目的特定提交或版本,对于go.mod中定义的依赖,则可以直接从对应仓库下载指定软件依赖,完成依赖分发。

存在问题:

  1. 无法保证构建确定性:软件作者可以直接在代码平台增删改软件版本,导致下次构建使用另外版本的依赖或者找不到依赖版本
  2. 无法保证依赖可用性:依赖软件作者可以直接在代码平台删除软件,导致依赖不可用
  3. 增加了第三方代码托管平台压力

2.2.6 依赖分发-proxy

Snipaste_2022-05-08_17-04-01.jpg go proxy 是一个服务站点,他会缓存源站中的软件,缓存的软件版本不会改变,并且在源站软件删除后依然可用,从而实现“immutability”和“available”的依赖分发。使用go proxy之后,构建项目时直接中proxy站点获取依赖。

Snipaste_2022-05-08_17-08-12.jpg GO Module通过GOPROXY变量控制使用Go Proxy。GPROXY是一个URL列表如上图所示。下载依赖时优先从proxy1下载依赖,如果proxy1不存在则在proxy2下载,proxy依旧不存在则最后会从源站下载,然后缓存到proxy站点中。
设置GOPROXY
go env -w GOPROXY=https://goproxy.cn,direct

2.2.7 工具-go get

  1. go get example.org/pkg
  2. @update 默认
  3. @none 删除依赖
  4. @v1.1.2 tag版本,语义版本
  5. @23dfdd5 特定的commit
  6. @master分支的最新commit

2.2.8 工具-go mod

go mod

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

3. 测试

  • 单元测试
  • mock测试
  • 基准测试

3.1 单元测试

Snipaste_2022-05-08_17-22-27.jpg

3.1.1 单元测试-规则

1.所有测试文件以_test.go结尾

Snipaste_2022-05-08_17-25-44.jpg 2.函数名定义为TestXxx

Snipaste_2022-05-08_17-25-54.jpg 3.初试化逻辑放到TestMain中

Snipaste_2022-05-08_17-26-55.jpg

3.1.2 单元测试-例子

被测试函数

Snipaste_2022-05-08_19-10-47.jpg 测试函数

Snipaste_2022-05-08_19-11-28.jpg 测试通过

Snipaste_2022-05-08_19-09-21.jpg 测试不通过

Snipaste_2022-05-08_19-09-47.jpg

3.1.3 单元测试-assert

使用testify中的assert

func TestHelloTom(t *testing.T) {
   output := HelloTom()
   expectOutput := "Tom"
   assert.Equal(t, expectOutput, output)
}

测试不通过

Snipaste_2022-05-08_19-14-28.jpg 测试通过

Snipaste_2022-05-08_19-14-54.jpg

3.1.4 单元测试-覆盖率

覆盖率是用来衡量代码是否经过了足够的测试,评价项目的评测水准,评估项目是否达到了高水准测试等级
覆盖率测试

//被测试代码
package test

func JudgePassLine(score int16) bool {
   if score >= 60 {
      return true
   }
   return false
}
//测试代码
package test

import (
   "github.com/stretchr/testify/assert"
   "testing"
)

func TestJudgePassLineTrue(t *testing.T) {
   isPass := JudgePassLine(70)
   assert.Equal(t, true, isPass)
}

使用go test 命令查看测试覆盖率
go test judgment_test.go judgment.go --cover

Snipaste_2022-05-08_19-26-49.jpg 加入新的测试方法,提高覆盖率

func TestJudgePassLineFail(t *testing.T) {
   isPass := JudgePassLine(50)
   assert.Equal(t, false, isPass)
}

Snipaste_2022-05-08_19-23-51.jpg Tips

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

3.2 单元测试-依赖

Snipaste_2022-05-08_19-40-01.jpg 幂等:是指每一次测试运行都应该产生与之前一样的结果 稳定:是指相互隔离,能在任何时间、任何环境,运行测试

3.3 单元测试-Mock

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

// Patch replaces a function with another
//target 目标函数
//replacement 替代函数
func Patch(target, replacement interface{}) *PatchGuard {
   t := reflect.ValueOf(target)
   r := reflect.ValueOf(replacement)
   patchValue(t, r)

   return &PatchGuard{t, r}
}
// Unpatch removes any monkey patches on target
// returns whether target was patched in the first place
func Unpatch(target interface{}) bool {
   return unpatchValue(reflect.ValueOf(target))
}

monkey patch的作用域在RunTime,在运行时通过GO的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,将待打桩函数的实现跳转打桩函数
使用例子

//被测试函数
package test

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
}
//mock测试
package test

import (
   "bou.ke/monkey"
   "github.com/stretchr/testify/assert"
   "testing"
)

func TestProcessFirstLine(t *testing.T) {
   firstLine := ProcessFirstLine()
   assert.Equal(t, "line00", firstLine)
}

func TestProcessFirstLineWithMock(t *testing.T) {
   monkey.Patch(ReadFirstLine, func() string {
      return "line110"
   })
   defer monkey.Unpatch(ReadFirstLine)
   line := ProcessFirstLine()
   assert.Equal(t, "line000", line)
}

通过patch对ReadFirstline进行打桩mock,默认返回line110,这里通过deferi卸载mock,这样整个测试函数就拜托了本地文件的束缚和依赖。

3.3 基准测试

基准测试是指测试一段程序的运行性能及耗费CPU的程度。在实际开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码进行性能分析,这就用到了基准测试。

标题:「Go 语言上手 - 工程实践」第三届字节跳动青训营 - 后端专场

网址:live.juejin.cn/4354/yc_app…