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

101 阅读5分钟

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

并发VS并行

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

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

Goroutine

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

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

Go协程:

go func

CSP

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

Channel

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

无缓冲通道 make(chan int)

有缓冲通道 make(chan int,2) 可解决生产和消费速度不均衡的执行效率问题 //defer 延迟资源关闭

并发安全Lock

addWithLock & addWithoutLock

不加锁的输出值不一定为预期结果

WaitGroup

Add(delta int) // 开启协程+1

Done() // 执行结束-1

Wait() // 主协程阻塞直到计数器为0

依赖管理

背景

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

Go依赖管理演进

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

环境变量$GOPATH

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

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

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

弊端

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

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

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

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

弊端

问题:

  • 无法控制依赖的版本
  • 更新项目又可能出现依赖冲突,导致编译出错
Go Module
  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包

终极目标:定义版本规则和管理项目依赖关系

依赖管理三要素

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

依赖标识:[Module Path][Version/pseudo-version]

依赖配置-version
  • 语义化版本

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

//不同MAJOR可不兼容,MINOR增补功能,PATCH修补bug

V1.3.0

V2.3.0

  • 基于commit伪版本

vX.0.0-yyyymmddhhmmss-abcdefgh1234

版本号-时间戳-哈希码

依赖配置-indirect

A->B->C

A->B 直接依赖

A->C 间接依赖 // indirect

依赖配置-incompatible

不同MAJOR版本间可以不相互兼容

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

项目中有依赖不同版本时,选择最低的兼容版本

依赖分发
依赖分发-回源

直接使用版本管理仓库下载依赖的问题:

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

在中间缓冲 稳定可靠

依赖分发-GOPROXY

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

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

工具
工具-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 增加需要的依赖,删除不需要的依赖

测试

  • 回归测试
  • 集成测试
  • 单元测试

从上到下,覆盖率逐层变大,成本却逐层降低

//单元测试的覆盖率一定程度反映代码的质量

单元测试
单元测试-规则
  • 所有测试文件以_test.go结尾
  • Func Text(*testing.T)
  • 初始化逻辑放到TestMain中
//测试前:数据装载、配置初始化等前置工作

code := m.Run()

//测试后:释放资源等收尾工作
单元测试-运行

go test [flags][packages]

单元测试-assert

eg. "githb.com/strechr/testify/assert"

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

外部依赖 => 稳定&幂等 // 幂等:重复运行函数,结果相同

// 稳定:单元测试在任何时间任何函数可独立运行

单元测试-Mock

Monkey: github.com/bouk/monkey

快速Mock函数

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

不再依赖本地文件

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

如rand函数会降低性能

Fastrand()---性能提高

项目实战

  • 展示话题和回帖列表
  • 暂不考虑前端页面实现,仅实现一个本地web功能
  • 话题和回帖数据用文件存储
ER图

实际运用时可参考理清思路

分层结构
  • 数据层:数据Model,外部数据的增删改查
  • 逻辑层:业务Entity,除了核心业务逻辑输出
  • 视图层:视图view,处理和外部的交互逻辑
组件工具
  • Gin高性能 go web框架

github.com/gin-gonic/g…

  • Go Mod

go mod init

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

Repository

参照ER图建立话题、回复的结构

  • 如何实现查询操作
Repository-index

元数据 索引

数据行 => 内存Map

var(

    topicIndexMap map[int64]*Topic

    postIndexMap  map[int64]*Post

}

//以初始化话题数据索引为例

func initTopicIndexMap(filePath string) error{

    open, ere := os.Open(filePath + "topic")

    if err != nil{

        return err

    }

    scanner := bufio.NewScanner(open)//基于file初始化scanner,遍历

    topicTmpMap := make(map[int64]*Topic)

    for scanner.Scan(){

        text := scanner.Text()

        var topic Topic

        if err := json.Unmarshal([]byte(text), &topic); err != nil{

            return err

    }//转化为结构体储存至内存map

    topicIndexMap = topicTmMap

    return nil

}
Repository-查询

索引:话题ID

数据:话题

var{

    topicDao  *TopicDao

    topicOnce sync.Once //sync适用于高并发场景下只执行一次的场景

}

func NewTopicDaoInstance() *TopicDao{

    topicOnce.Do(

        func(){

            topicDao = &TopicDao{}

        })

    return topicDao

}

func(*TopicDao) QueryTopicById(id int64) *Topic{

    return topicIndexMap[id]

}
Service

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

topic与post无先后依赖关系,故可并行处理

func(f *QueryPageInfoFlow) prepareInfo() error{

    //获取topic信息

    var wg sync.WaitGroup

    wg.Add(2)

    go func(){...}()

    //获取post列表

    go func(){...}()

    wg.Wait()

    return nil

}
Controller
  • 构建view对象
  • 业务错误码
Router
  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务
func main(){

    if err := Init("./data/"); err != nil{

        os.Exit(-1)

    }//初始化数据索引

    r := gin.Dafault()//初始化引擎配置

    r.GET("community/page/get/:id", func(c *gin.Context){

        topicId:= c.Param("id")

        data := controller.QueryPageInfo(topicId)

        c.JSON(200, data)

    })//构建路由

    err := r.Run()//启动服务

    if err != nil{

        return

    }

}
运行

go run server.go