Go语言入门-工程实践 | 青训营笔记

72 阅读4分钟

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

语言进阶 从并发编程的视角了解Go高性能的本质

依赖管理 了解Go语言依赖管理的演进路线

测试 从单元测试实践出发,提升质量意识

项目实战 通过项目需求,需求拆解,逻辑设计,代码实现感受真正的项目开发

01语言进阶 并发编程

01并发vs并行

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

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

1.1Goroutine

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

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

快速打印 快速:开多个协程去打印

go语言开启一个协程:在调用函数时,在函数前面加上一个go关键字

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

输出 image.png

1.2CSP(Communicating Sequential Processes)

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

image.png

通过通信共享内存:通道

通过共享内存实现通信:临界区

1.3Channel

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

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

image.png

实现

package main

func CalSquare() {
   src := make(chan int)
   dest := make(chan int, 3)
   //A 子协程发送0~9数字
   go func() {
      defer close(src)
      for i := 0; i < 10; i++ {
         src <- i
      }
   }()
   //B 子协程计算输入数字的平方
   go func() {
      defer close(dest)
      for i := range src {
         dest <- i * i
      }
   }()
   //主协程输出最后的平方数
   for i := range dest {
      //复杂操作
      println(i)
   }
}

func main() {
   CalSquare()
}

输出

image.png

1.4并发安全Lock

package main

import (
   "sync"
   "time"
)

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

func Add() {
   x = 0
   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
   time.Sleep(time.Second)
   println("WithoutLock:", x)
   x = 0
   for i := 0; i < 5; i++ {
      go addWithLock()
   }
   time.Sleep(time.Second)
   println("WithLock:", x)
}

func main() {
   Add()
}

输出

image.png

1.5WaitGroup

三个方法: Add(计数器+delta) Done(计数器-1) Wait(阻塞直到计数器为0)

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

package main

import (
   "fmt"
   "sync"
)

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

func ManyGoWait() {
   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()
}

func main() {
   ManyGoWait()
}

输出

image.png

02依赖管理

2.1Go依赖管理演进

image.png

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

2.1.1GOPATH

  • 环境变量 $GOPATH bin:项目编译的二进制文件

pkg:项目编译的中间产物,加速编译

src:项目源码

  • 项目代码直接依赖src下的代码
  • go get下载最新版本的包到src目录下

弊端

场景:A和B依赖于某一package的不同版本

问题:无法实现package的多版本控制

2.1.2Go Vendor

  • 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
  • 依赖寻址方式:vendor=>GOPATH(项目依赖会先到verdor下获取,没有再到GOPATH) 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题

弊端

image.png

问题:

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

2.1.3Go Module

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

2.2依赖管理三要素

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

2.3依赖配置

2.3.1依赖配置-go.mod

image.png

2.3.2依赖配置-version

  • 语义化版本 ${MAJOR}.${MINOR}.${PATCH}

V1.3.0

V2.3.0

  • 基于commit伪版本 vx.0.0-yyyymmddhhmmss-abcdefgh1234

v0.0.0-20220401081311-c38fb59326b7

v1.0.0-20201130134442-10cb98267c6c

2.3.3依赖配置-indirect

image.png

2.3.4依赖配置-incompatible

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

依赖图

image.png 选择最低的兼容版本

2.3依赖分发

2.3.5依赖分发-回源

image.png

2.3.5依赖分发-Proxy

image.png

2.3.6依赖分发-变量 GOPROXY

GOPROXY="proxy1.cn, proxy2.cn ,direct"

服务站点URL列表,“direct"表示源站

Proxy1->Proxy2->Direct

2.3工具

2.3.7工具-go get

go get example.org/ pkg

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

2.3.8工具-go mod

go mod

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

03测试

3.1单元测试

image.png

3.1.1规则

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

3.1.2例子

package main

import "testing"

func HelloTom() string {
   return "Jerry"
}

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

输出

image.png

3.1.3运行

go test[flags][packages]

3.1.4单元测试-assert

package main

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

func HelloTom() string {
   return "Tom"
}

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

3.1.5单元测试-覆盖率

package main

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

func JudgePassLine(score int16) bool {
   if score >= 60 {
      return true
   }
   return false
}

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

3.1.5单元测试-Tips

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

3.2单元测试-依赖

image.png

3.3单元测试-文件处理

package main

import (
   "bufio"
   "os"
   "strings"
   "testing"
)

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
}

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

3.4单元测试-Mock

快速Mock函数

  • 为一个函数打桩
  • 为一个方法打桩
package main

import (
   "reflect"
   "testing"
)

func Patch(target, replacement interface{}) *PatchGuard {
   t := reflect.ValueOf(target)
   t := reflect.ValueOf(replacement)
   patchValue(t, r)

   return &PatchGuard{t, r}
}

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

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

对ReadFirstLine打桩测试,不再依赖本地文件

3.5基准测试

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

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

import (
   "math/rand"
   "testing"
)

/**
随机选择执行服务器
*/
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 BenchmarkSelect(b *testing.B) {
   InitServerIndex() //初始化服务器列表
   b.ResetTimer()    //定时器重置 InitServerIndex不属于测试服务器的损耗 去掉
   for i := 0; i < b.N; i++ {
      Select()
   }
}

//并行 性能有劣化
//原因:Select sdk 用到了rand函数 rand函数为了保证全局的随机性和并发安全 持有一把全局锁
func BenchmarkSelectParallel(b *testing.B) { //并行
   InitServerIndex()
   b.ResetTimer()
   b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
         Select()
      }
   })
}

优化

//fastrand 牺牲了一定的随机数列的一致性
func FastSelect() int {
   return ServerIndex[fastrand.Intn(10)]
}

04项目实践

4.1需求描述

社区话题页面

  • 展示话题(标题,文字描述)和回帖列表
  • 暂不考虑前端页面实现,仅仅实现一一个本地web服务
  • 话题和回帖数据用文件存储

4.2需求用例

4.3ER图-Entity Relationship Diagram

4.4分层结构

  • 数据层:数据Model,外部数据的增删改查
  • 逻辑层:业务Entity,处理核心业务逻辑输出
  • 视图层视图view,处理和外部的交互逻辑

4.5组件工具

  • Gin高性能go web框架
  • Go Mod

4.6Repository

index

查询

4.7Service

实体

流程:参数校验->准备数据->组装实体

4.8Controller

构建view对象

业务错误码

4.9Router

初始化数据索引

初始化引擎配置

构建路由

启动服务

4.10运行