语言进阶、依赖管理和工程测试 | 青训营笔记

114 阅读5分钟

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


昨天使用Go 语言基础实现了猜谜游戏、在线词典、SOCKS5代理,并且加以巩固记忆。

今天我将对于Go 语言进阶、依赖管理以及工程实践测试做出总结


一.语言进阶——并发编程

1. 并发 VS 并行

image-20230113143138484

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

image-20230113143147957

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

2. Goroutine

image-20230113143401412

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

线程:内核态,线程可以跑多个协程,栈 MB 级别

快速打印:

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    HelloGoroutine()
}
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)
}

3.CSP(Communicating Sequential Processes)

image-20230113144943055

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


4. Channel 通道

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

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

image-20230113145409520

例子:

A 子协程发送0~9数字

B 子协程计算输入数字的平方

主协程输出最后的平方数

package main
​
func main() {
    CalSquare()
}
func CalSquare() {
    src := make(chan int)
    dest := make(chan int, 2)
    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)
    }
}

5. 并发安全 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()
}

6.WaitGroup

image-20230113151531091

计数器

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

快速打印优化:

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

二.依赖管理

1.背景

image-20230113152304360

  • 工程项目不可能基于标准库0~1编码构建
  • 管理依赖库

2. 依赖管理演进

image-20230113152430437

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

2.1.1 GOPATH

✔ 环境变量 $GOPATH

  • bin:项目编译的二进制文件
  • pkg:项目编译的中间产物
  • src:项目源码

✔ 项目代码直接依赖 src 下的代码

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


2.1.2 GOPATH弊端

image-20230113152937366

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

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


2.1.3 GO Vendor

  • 项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor
  • 依赖寻址方式: vendor => GOPATH

image-20230113153052167

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


2.1.4 GO Vendor 弊端

image-20230113153347302

问题:

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

3. GO Moudle

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

3.1 依赖管理三要素

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

3.2 依赖配置——go.mod

image-20230113153730652

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


3.3 依赖配置——version

语义化版本

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

>V1.3.0

>V2.3.0

基于 commit 伪版本

> vx.0.0-yyyymmddhhmmss-abcdefgh1234

3.4 依赖配置——indirct

image-20230113154215583

             { A -> B 直接依赖
A -> B -> C   
             { A -> C 简介依赖

对于go.mod,没有直接导入的依赖模块就会被标识为 indirect

3.5 依赖配置——incompatible

image-20230113154457850

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

3.6 依赖分发——回源

image-20230113155233113

  • 无法保证构建稳定性

    • 增加/修改/删除软件版本
  • 无法保证依赖可用性

    • 删除软件
  • 增加第三方压力

    • 代码托管平台负载问题

3.7 依赖分发——Proxy

image-20230113155529019

3.8 依赖分发——变量 GOPROXY

GOPROXY="proxy1.cn,https://proxy2.cn,…"

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

Proxy 1  ---> Proxy 2 ---> Direct

4.工具

4.1 工具——go get

go get example.prg/pkg {@...}
    
    @update    默认
    @none      删除依赖
    @v1.1.2    tag版本,语义版本
    @23dfdd5   特定的commit
    @master    分支的最新commit

4.2 工具——go mod

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

三.测试

1. 单元测试

image-20230113161014207

1.1 单元测试——规则

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

image-20230113161244957

  • func TestXxx(*testing.T)

func TestPublishPost(t *testing.T) {}

  • 初始化逻辑放在TestMain中
func TestMain(m *testing.M) {
    //测试前:数据装载、配置初始化等前置工作
    
    code := m.Run()
    
    //测试后:释放资源等收尾工作
    
    os.Exit(code)
}

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

1.3 单元测试——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)
}

1.4 单元测试——覆盖率

package main
​
import (
    "github.com/stretchr/testify/assert"
    "testing"
)
​
func TestJudgePassLineTrue(t *testing.T) {
    isPass := JudgePassLine(70)
    assert.Equal(t, true, isPass)
}
func TestJudgePassLineFail(t *testing.T) {
    isPass := JudgePassLine(30)
    assert.Equal(t, false, isPass)
}

1.5 单元测试——Tips

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

1.6 单元测试——依赖

image-20230113163311018

                                            外部依赖 >= 稳定&幂等

1.7 单元处理——文件处理

package main
​
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
}
package main
​
import (
    "github.com/stretchr/testify/assert"
    "testing"
)
​
func TestProcessFirstLine(t *testing.T) {
    firstLine := ProcessFirstLine()
    assert.Equal(t, "line00", firstLine)
}

2. Mock 测试

开源测试包:Monkey | monkey:github.com/bouk/monkey

快速Mock函数

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

image-20230113170505101

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

3. 基准测试

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

3.1 基准测试——例子

image-20230113170754277

3.2 基准测试——运行

image-20230113170845816

3.4 基准测试——优化

image-20230113171234529

四.项目实战

1. 需求设计

1.1 需求描述

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

1.2 需求用例

浏览消费用户

image-20230113171814245

1.3 E-R图

image-20230113171911702

1.4 分层结构

image-20230113172108473

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

1.5 组件工具

✔ Gin 高性能 go web 框架

https://github/gin-gonic/gin#installation

✔ Go Mod

go mod init

go get gopkg.in/gin-gonic/gin.v1@v1.3.0

2. 代码开发

2.1 Repository

image-20230113173322860

2.2 Repository——index

image-20230113174013699

2.3 Repository——查询

var (
    topicDao  *TopicDao
    topicOnce sync.Once //类似于单例模式
)
​
func NewTopicDaoInstance() *TopicDao {
    topicOnce.Do(
        func() {
            topicDao = &TopicDao{}
        })
    return topicDao
}
//参数为 话题ID
func (*TopicDao) QueryTopicById(id int64) *Topic {
    return topicIndexMap[id]
}

2.4 Service

实体

type PageInfo struct {
    Topic    *repository.Topic
    PostList []*repository.Post
}

流程

image-20230114145300299

代码流程编排

func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
   if err := f.checkParam(); err != nil {
      return nil, err
   }
   if err := f.prepareInfo(); err != nil {
      return nil, err
   }
   if err := f.packPageInfo(); err != nil {
      return nil, err
   }
   return f.pageInfo, nil
}

可用性

并行处理image-20230114145436998

func (f *QueryPageInfoFlow) prepareInfo() error {
    //获取topic信息
    var wg sync.WaitGroup
    wg.Add(2)
    //启用协程
    go func() {
        defer wg.Done()
        topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
        f.topic = topic
    }()
    //获取post列表
    //启用协程
    go func() {
        defer wg.Done()
        posts := repository.NewPostDaoInstance().QueryPostsByParentId(f.topicId)
        f.posts = posts
    }()
    wg.Wait()
    return nil
}

2.5 Controller

✔ 构建 View 对象

✔ 业务错误码

type PageData struct {
    Code int64       `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data"`
}
​
func QueryPageInfo(topicIdStr string) *PageData {
    topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
    if err != nil {
        return &PageData{
            Code: -1,
            Msg:  err.Error(),
        }
    }
    pageInfo, err := service.QueryPageInfo(topicId)
    if err != nil {
        return &PageData{
            Code: -1,
            Msg:  err.Error(),
        }
    }
    return &PageData{
        Code: 0,
        Msg:  "success",
        Data: pageInfo,
    }
​
}

2.6 Router

✔ 初始化数据索引

✔ 初始化引擎配置

✔ 构建路由

✔ 启动服务

func main() {
    if err := Init("./data/"); err != nil {
        os.Exit(-1)
    }
    r := gin.Default()
    r.GET("/community/page/get/:id", func(c *gin.Context) {
        topicId := c.Param("id")
        data := cotroller.QueryPageInfo(topicId)
        c.JSON(200, data)
    })
    err := r.Run()
    if err != nil {
        return
    }
}
​
func Init(filePath string) error {
    if err := repository.Init(filePath); err != nil {
        return err
    }
    return nil
}

3. 测试运行

image.png

image.png