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

160 阅读9分钟

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

主要针对《Go 语言上手-工程实践》进行学习和总结

1 并发编程

1.1 Goroutine

  • 线程 vs 协程

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

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

goroutine.png

  • 开启:go关键字

1.2 CSP

  • CSP:Communicating Sequential Processes,实现协程通信

1.3 Channel - 通信实现共享内存

  • 格式:make(chan T, [size]),T为元素类型,size为缓冲大小

  • 分类:

    • 无缓冲通道:make(chan T),也称为同步通道

    • 有缓冲通道:make(chan T, size),可以解决执行效率不同的问题

channel.png

```
package main
​
import "fmt"
​
func main() {
    // 实现producer和consumer的通信
    src := make(chan int)
    // 此处考虑consumer的消费速度要慢于producer的生产速度
    // 使用带缓冲的队列,避免因为consumer的消费速度问题影响producer的生产速度
    dest := make(chan int, 3)   
​
    // producer
    go func() {
        defer close(src)
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
​
    // consumer
    go func() {
        for i := range src {
            dest <- i * i
        }
    }()
​
    // main
    for i := range dest {
        fmt.Println(i)
    }
}
​
```

1.4 Lock - 共享内存实现通信

  • sysc.Mutex
package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
var (
    x    int64
    lock sync.Mutex
)
​
func main() {
    addWithoutLock := func() {
        for i := 0; i < 2000; i++ {
            x += 1
        }
    }
​
    addWithLock := func() {
        for i := 0; i < 2000; i++ {
            lock.Lock()
            x += 1
            lock.Unlock()
        }
    }
​
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    fmt.Println("WithoutLock:", x)
​
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    fmt.Println("WithLock:", x)
}

1.5 WaitGroup

  • type WaitGroup struct {
        noCopy noCopy
        state1 [3]uint32
    }
    ​
    func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {}
    func (wg *WaitGroup) Add(delta int) {}
    func (wg *WaitGroup) Done() {}
    func (wg *WaitGroup) Wait() {}
    

2 依赖管理

2.1 GOPATH

  • 环境变量:$GOPATH

  • 目录结构:

    bin:项目编译的二进制文件

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

    src:项目源码,通过go get命令下载最新的包到src目录中

  • 缺点:当项目A和B依赖于某个package的不同版本时,GOPATH无法实现package的多版本控制

    GOPATH缺陷.png

2.2 Go Verdor

  • 方式:在项目目录下,增加verdor目录,项目中依赖的包以副本形式放在 $ProjectRoot/vendor

  • 寻址方式:vendor -> GOPATH,依赖包源码仍然存放在GOPATH中

  • 优点:解决了多项目依赖同一package不同版本的冲突问题

  • 缺点:项目A依赖package B和C,而B和C依赖于同一个package D的不同版本,导致无法控制依赖的版本,同时更新项目时可能出现依赖冲突,导致编译错误

govendor缺陷.png

2.3 Go Module

  • 方式:

    • go.mod:管理依赖包版本,配置文件
    • proxy:中心仓库
    • go get/go mod命令:管理依赖包
  • go.mod:参考go.dev/ref/mod

    // module path
    module example/project/app
    ​
    // go directive
    go 1.16
    ​
    // require-单元依赖
    require (
        example/lib1 v1.0.2
        example/lib2 v1.0.0 // indirect
        example/lib3 v0.0.0-20190725025543-5a5fe074e612
        example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd // indirect
        example/lib5/v3 v3.0.2
        example/lib6 v3.2.0+incompatible
    )
    
    • 语义化版本规范2.0.0

      major.minor.path-beta+metadata:主版本号.次版本号.修订号-先行版本号+版本编译信息

      主版本号:不兼容的API修改

      次版本号:向下兼容的功能性新增

      修订号:向下兼容的问题修正

      先行版本号(可选):用-相连与前面,表示版本非稳定,可能无法满足兼容性要求

      版本编译信息(可选):用+相连与前面,并用.分割的标识符修饰。当判断版本优先级时会被忽略

    • 基于commit伪版本:

      vx.0.0-yyyymmddhhmmss-abcdefgh1234:版本号-时间戳-哈希校验码

    • module path:工程模块的名称,包括:

      • repository路径
      • repository文件夹
      • 版本:版本为v1时默认不写,大于v1时用vx区分(require中同理)

      格式:.repo_url/.nameOfDir/.version

      e.g. github.com/dir_a/dir_b/v2

    • go directive:设置预期使用的go语言版本,编译器在编译包时就知道使用哪个版本的go去编译

    • require:指定module依赖的最小需要的版本

      // indirect:主module中的任何包都没有直接import该依赖,即间接依赖

      +incompatible:没有go.mod文件且主版本号大于v1的依赖

  • MVS:Minimal version selection,最小版本选择,go构建package时选择module版本的算法

    参考:research.swtch.com/vgo-mvs#low…

  • 依赖分发 - proxy

    • GOPROXY:包含服务站点URL列表,direct表示源站

依赖分发-proxy.png

  • go get

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

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

3 测试

单元测试:完成最小的软件设计单元(模块)的验证工作,目标是确保模块被正确的编码,使用过程设计描述作为指南,对重要的控制路径进行测试以发现模块内的错误,通常情况下是白盒的,对代码风格和规则、程序设计和结构、业务逻辑等进行静态测试,及早的发现和解决不易显现的错误。

集成测试:通过测试发现与模块接口有关的问题。目标是把通过了单元测试的模块拿来,构造一个在设计中所描述的程序结构,应当避免一次性的集成(除非软件规模很小),而采用增量集成。

  • 自顶向下集成:模块集成的顺序是首先集成主模块,然后按照控制层次结构向下进行集成,隶属于主模块的模块按照深度优先或广度优先的方式集成到整个结构中去。
  • 自底向上集成:从原子模块开始来进行构造和测试,因为模块是自底向上集成的,进行时要求所有隶属于某个给顶层次的模块总是存在的,也不再有使用稳定测试桩的必要。

回归测试:在发生修改之后重新测试先前的测试用例以保证修改的正确性。理论上,软件产生新版本,都需要进行回归测试,验证以前发现和修复的错误是否在新软件版本上再次出现。根据修复好了的缺陷再重新进行测试。回归测试的目的在于验证以前出现过但已经修复好的缺陷不再重新出现。一般指对某已知修正的缺陷再次围绕它原来出现时的步骤重新测试。

3.1 单元测试

单元测试.png

  • 规则:

    • 测试文件以_test.go结尾

    • 测试函数以func TestXxx(*testing.T)的方式定义

    • 初始化逻辑放在TestMain中,func TestMain(*testing.M),在testing.T执行之前执行

      func TestMain(m *testing.M) {
          // 测试前:数据装载、配置初始化等前置工作
          code := m.Run()
          // 测试后:释放资源等收尾工作
          os.Exit(code)
      }
      
  • 测试命令:go test [flags] [packages],flags为_test.go,packages为被测试包

  • assert包:测试结果的校对

  • 覆盖率:--cover参数,测试能够覆盖被测试包中有效代码的行数占整体行数的比例

    tips:测试分支相互独立、全面覆盖;测试单元粒度足够小,要求被测试包中的函数单一职能

3.2 Mock测试

  • 确保测试的幂等稳定

    幂等:重复运行测试的结果相同

    稳定:单元测试相互隔离,即在任何时间、对任何函数,测试可以独立运行、互不影响

  • mock实现了不依赖于测试文件、本地环境的测试

  • monkey:github.com/bouk/monkey

    功能:函数打桩,方法打桩

    // Patch replaces a function with another
    // 将target函数替换为replacement,即函数地址的替换,测试时使用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))
    }
    

3.3 基准测试

  • 作用:测试程序运行时的性能、CPU损耗等

  • 格式:

    • 测试函数BenchmarkXxx(*testing.B)

      并行:在测试函数中,b.RunParallel(func(*testing.PB){})

  • 随机:github.com/bytedance/g…中的fastrand

4 项目实践

4.1 需求设计

  1. 需求描述

    社区话题页面

    • 展示话题Topic(标题,文字描述)和回帖列表PostList
    • 话题和回帖数据通过文件存储
  2. 需求用例:UML

UML.png

  1. ER图:Entity Relationship Diagram

ER图.png

  1. 分层结构

分层结构.png

数据层:数据Model,外部数据的增删改查

逻辑层:业务Entity,处理核心业务逻辑输出

视图层:视图View,处理与外部的交互逻辑

0. 组件工具

-   Gin高性能go web框架

    <https://github.com/gin-gonic/gin>

-   Go Mod:

    ```
    go mod init
    go get https://github.com/gin-gonic/gin.v1@v1.3.0
    ```

4.2 代码开发

4.2.1 Repository

完成数据实体和数据DAO(Data Access Object)的设计

  • User:

    type User struct {
        Id         int64     `gorm:"column:id"`
        Name       string    `gorm:"column:name"`
        Avatar     string    `gorm:"column:avatar"`
        Level      int       `gorm:"column:level"`
        CreateTime time.Time `gorm:"column:create_time"`
        ModifyTime time.Time `gorm:"column:modify_time"`
    }
    

    UserDao:

    type UserDao struct {}
    // 查询用户id的信息
    func (*UserDao) QueryUserById(id int64) (*User, error) {}
    // 查询多用户id的信息
    func (*UserDao) MQueryUserById(ids []int64) (map[int64]*User, error){}
    
  • Topic:

    type Topic struct {
        Id         int64     `gorm:"column:id"`
        UserId     int64     `gorm:"column:user_id"`
        Title      string    `gorm:"column:title"`
        Content    string    `gorm:"column:content"`
        CreateTime time.Time `gorm:"column:create_time"`
    }
    

    TopicDao:

    type TopicDao struct {}
    // 查询话题id的信息
    func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {}
    
  • Post:

    type Post struct {
        Id         int64     `gorm:"column:id"`
        ParentId   int64     `gorm:"parent_id"`
        UserId     int64     `gorm:"column:user_id"`
        Content    string    `gorm:"column:content"`
        DiggCount  int32     `gorm:"column:digg_count"`
        CreateTime time.Time `gorm:"column:create_time"`
    }
    

    PostDao:

    type PostDao struct {}
    // 查询回帖id的信息
    func (*PostDao) QueryPostById(id int64) (*Post, error) {}
    // 查询话题id对应的话题中所包含的所有回帖信息
    func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) {}
    // 创建一个回帖
    func (*PostDao) CreatePost(post *Post) error {}
    
  • 单例模式创建Dao

    var xxxDao *XxxDao
    var xxxOnce sync.Once
    ​
    func NewXxxDaoInstance() *XxxDao {
        xxxOnce.Do(
            func() {
                xxxDao = &XxxDao{}
            })
        return xxxDao
    }
    

    关于sync.Once:确保传入的函数只执行一次

    type Once struct {
        
        done uint32
        m    Mutex
    }
    ​
    // 传入仅需执行一次的func
    func (o *Once) Do(f func()) {
        // 原子操作,判断done是否为0
        if atomic.LoadUint32(&o.done) == 0 {
            o.doSlow(f)
        }
    }
    ​
    func (o *Once) doSlow(f func()) {
        // 加锁
        o.m.Lock()
        defer o.m.Unlock()
        if o.done == 0 {
            // func执行完毕后,将done修改为1
            defer atomic.StoreUint32(&o.done, 1)
            f()
        }
    }
    
  • gorm:

4.2.2 Service

  • 基本流程:

逻辑层-流程.png

  • 数据流结构体:对不同数据信息进行封装

    type TopicInfo struct {
        Topic *repository.Topic
        User  *repository.User
    }
    ​
    type PostInfo struct {
        Post *repository.Post
        User *repository.User
    }
    ​
    type PageInfo struct {
        TopicInfo *TopicInfo
        PostList  []*PostInfo
    }
    ​
    type QueryPageInfoFlow struct {
        topicId  int64     // 根据topicId查询对应页面的信息
        PageInfo *PageInfo // 数据流// 临时变量
        topic   *repository.Topic
        posts   []*repository.Post
        userMap map[int64]*repository.User
    }
    
  • 参数校验:对查询的输入进行检查,防止SQL注入或验证数据的有效性

    func (f *QueryPageInfoFlow) checkParam() error {
        if f.topicId <= 0 {
            return errors.New("topic id must be larger than 0")
        }
        return nil
    }
    
  • 准备数据:查询获取DAO层的数据,并封装为已定义好的结构体

    func (f *QueryPageInfoFlow) prepareInfo() error {
        // 多协程同时获取topic和post信息
        var wg sync.WaitGroup
        wg.Add(2)
    ​
        var topicErr, postErr error// 获取topic信息
        go func() {
            defer wg.Done()
            topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
            if err != nil {
                topicErr = err
                return
            }
            f.topic = topic
        }()
    ​
        // 获取post列表
        go func() {
            defer wg.Done()
            posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId)
            if err != nil {
                postErr = err
                return
            }
            f.posts = posts
        }()
    ​
        wg.Wait()
    ​
        if topicErr != nil {
            return topicErr
        }
        if postErr != nil {
            return postErr
        }
    ​
        // 获取用户信息
        uids := []int64{f.topic.UserId}
        for _, post := range f.posts {
            uids = append(uids, post.UserId)
        }
        userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
        if err != nil {
            return err
        }
        f.userMap = userMap
        return nil
    }
    
  • 组装实体:将数据组装为数据流传输实体

    func (f *QueryPageInfoFlow) packPageInfo() error {
        // topic info
        userMap := f.userMap
        topicUser, ok := userMap[f.topic.UserId]
        if !ok {
            return errors.New("has no topic user info")
        }
    ​
        // post list
        postList := make([]*PostInfo, 0)
        for _, post := range f.posts {
            postUser, ok := userMap[post.UserId]
            if !ok {
                return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
            }
            postList = append(postList, &PostInfo{
                Post: post,
                User: postUser,
            })
        }
    ​
        // 组装
        f.PageInfo = &PageInfo{
            TopicInfo: &TopicInfo{
                Topic: f.topic,
                User:  topicUser,
            },
            PostList: postList,
        }
        return nil
    }
    
  • 调用机制:

    func QueryPageInfo(topicId int64) (*PageInfo, error) {
        return NewQueryPageInfoFlow(topicId).Do()
    }
    ​
    func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
        return &QueryPageInfoFlow{
            topicId: topId,
        }
    }
    ​
    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
    }
    

4.2.3 Controller

  • 与view交互的数据结构

    type PageData struct {
        Code int64       `json:"code"` // error定义
        Msg  string      `json:"msg"`  // error信息
        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(),
            }
        }
    ​
        // 获取service层结果
        pageInfo, err := service.QueryPageInfo(topicId)
        if err != nil {
            return &PageData{
                Code: -1,
                Msg:  err.Error(),
            }
        }
        return &PageData{
            Code: 0,
            Msg:  "success",
            Data: pageInfo,
        }
    }
    

4.2.4 Router

  • 初始化数据索引

    if err := Init(); err != nil {
        os.Exit(-1)
    }
    ​
    func Init() error {
        if err := repository.Init(); err != nil {
            return err
        }
        if err := util.InitLogger(); err != nil {
            return err
        }
        return nil
    }
    
  • 初始化引擎配置

    r := gin.Default()
    
  • 构建路由

    r.GET("/community/page/get/:id", func(c *gin.Context) {
        topicId := c.Param("id")
        data := handler.QueryPageInfo(topicId)
        c.JSON(200, data)
    })
    
  • 启动服务

    err := r.Run()
    if err != nil {
        return
    }
    

4.3 测试运行

  • 测试函数
func TestQueryPageInfo(t *testing.T) {
    // 被测试函数的输入参数
    type args struct {
        topicId int64
    }
​
    // 测试用例
    tests := []struct {
        name    string
        args    args
        wantErr bool
    }{
        {
            name: "查询页面",
            args: args{
                topicId: 1,
            },
            wantErr: false,
        },
    }
​
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := QueryPageInfo(tt.args.topicId) // 被测试的函数
            if (err != nil) != tt.wantErr {
                t.Errorf("QueryPageInfo() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
        })
    }
}

分层结构.png

  • 运行命令:curl --location --request GET 'http://0.0.0.0:8080/community/page/get/2' | json