后端与 Go 语言进阶 | 青训营笔记

82 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第2天,今天学习的是进阶相比昨天的基础难度上要大上一点,不过因为字节提供了丰富的学习资料和视频,学习起来还是比较顺利的,以下便是我今天的收获

工程实践

1.1 语言进阶

并发 VS 并行

  • 并发:多线程程序在一个核的cpu上运行,实现方式就是时间片的切换达到同时运行的状态
  • 并行:直接运用多核实现多线程的同时运行,可以理解成实现并发的一个手段
  • Go语言实现了性能极高的并发调度模型,通过高效的调度可以最大程度的利用计算资源,Go可以充分发挥多核优势。

1.1.1 Goroutine

  • 线程:属于内核态,创建、切换、停止都属于很重的系统操作,比较消耗资源,栈内存在MB级别,此外线程可以并发的跑多个协程
  • 协程:可以理解为轻量级的线程,创建和调度由Go语言本身完成,栈内存在KB级别
func hello(i int) {
    println(fmt.Sprint(i))
}

func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

 time.Sleep(time.Second)是为了子携程在执行完之前,主线程不退出

1.1.2 CSP

Go提倡通过通信共享内存,而不是通过共享内存而实现通信,因为后者会发生数据静态的问题,在一定程度上可能会影响性能

2023-01-16-13-23-30-image.png 注:数据静态问题指的是由于数据库结构不同或者是由于数据不断发生变化而导致的系统及其功能的不稳定性或问题

1.1.3 Channel

创建方式

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

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int, 2)
  • 无缓冲通道又叫同步通道,有缓冲通道可以说是来解决同步问题的

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

以上的实现方式分别设置了两个缓冲通道,其中有缓冲通道的作用是为了解决生产-消费冲突

1.1.4 并发安全 Lock

多个协程同时操作一个个内存资源的情况

加锁和不加锁最大的区别就在于,不加锁可能会导致并发安全的问题

var (
    x int64
    lock sync.Mutex
)
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
func addWithLock() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}

开5个协程之后,加锁的会输出10000,而不加锁会输出一个不确切的数,这便是并发安全问题,解决方式就是加锁

1.1.5 WaitGroup

Add(dealta int):计数器+delta

Done():计数器-1

Wait():阻塞直到计数器为0

func ManyGoWait() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            deder wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}

1.2 依赖管理

1.2.1 背景

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

1.2.2 Go依赖管理演进

  • 不同环境依赖的版本不同
  • 控制依赖库的版本

1.2.3 GOPATH

  • 环境变量$GOPATH

    • bin:项目编译的二进制文件
    • pkg:项目编译的中间产物,加速编译、
    • src:项目源码
  • 项目代码直接依赖src下的代码

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

弊端

无法实现package的多版本控制,因为项目A和B依赖于某一package的不同版本

1.2.4 Go Vendor

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

弊端

无法控制依赖版本,更新项目由可能出现依赖冲突,导致这个问题的原因其实是因为Vendor还是依赖源码实现的

1.2.5 Go Module

  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包
  • 可以实现定义版本规则和管理项目依赖关系

1.2.6 依赖管理三要素

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

go 1.19 //原生库

require ( //单元依赖
    example/lib1 v1.0.1
    example/lib2 v1.0.2
) 

1.2.7 依赖配置-version

  • 语义化版本

    • MAJOR.MINOR.PATCH
    • MAJOR:可以理解为大版本,不同的MAJOR是代码隔离的
    • MINOR:做一些功能,在MAJOR下需要前后兼容
    • PARCH:做一些代码bug的修复
  • 基于commit伪版本

    • 版本前缀:和语义化版本是一样的
    • 中间部分:时间戳
    • 版本后缀:提交后的一个12位的哈希码的前缀

1.2.8 依赖配置-indirect

通俗来说就是没有指定直接依赖的间接依赖

1.2.9 依赖配置-incompatile

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

1.2.9 依赖配置-依赖图

2023-01-16-15-08-47-image.png

选择最低兼容版本

1.2.10 依赖分发-回源

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

1.2.11 依赖分发-Proxy

  • Proxy就是建立了一个缓存内容

1.2.12 依赖分发-变量 GOPROXY

GOPROXY="[proxy1.cn,https://proxy2.cn,…] 服务站点URL列表,“direct”表示源站

1.3 测试

测试这个环节是非常重要的,可以大幅度的减少事故的发生

1.3.1 单元测试

单元测试是成本最小的测试,可以对接口、函数、模块等进行测试,用来保证代码的功能能达到期望

1.3.2 单元测试-规则

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

1.3.3 单元测试-例子

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

1.3.4 单元测试-运行

在命令行中的运行方式

go test [flags][packages]

在开发工具中只要函数命名格式正确就可以直接运行

1.3.5 单元测试-assert

import (
    "github.com/stretchr/testify/assert"
    "testing"
)
func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    assert.Equal(t, expectOutput, output)
}
func HelloTom() string {
    return "Tom"
}

此时引入assert包实现期望和实际测试结果的比较

1.3.6 单元测试-覆盖率

  • 下面由一段代码来解释
func JudgePassLine(score int16) bool {
    if score >= 60 {
        return true
    }
    return false
}
func TestJudgePassLineTrue(t *testing.T) {
    isPass := JudgePassLine(70)
    assert.Equal(t, true, isPass)
}

此时结果覆盖率为66.7%,为什么是这个结果,因为上述代码中70大于60,然后返回true,后面的retuen false就不会执行了所以三行函数体代码只执行了两行所以是2/3,即66.7%

  • 覆盖率也可以进行提高,我由一段代码来说明
func TestJudgePassLineTrue(t *testing.T) {
    isPass := JudgePassLine(70)
    assert.Equal(t, true, isPass)
}
func TestJudgePassLineTrue(t *testing.T) {
    isPass := JudgePassLine(50)
    assert.Equal(t, false, isPass)
}

这样返回的覆盖率就是100%了,但在实际开发过程中不会出现这么简单提升的情况

1.3.7 单元测试-Tips

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

1.3.8 单元测试-依赖

  • 稳定:相互隔离,能在任何时间和环境下运行测试
  • 幂等:每一次测试运行都应该产生与之前一样的结果

1.3.9 单元测试-Mock

快速Mock函数就是为一个函数打桩,打桩的意思其实可以从字面意思来理解,即把函数打进去用打桩的接口来替代原函数的作用

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

上述代码对ReadFirstLine打桩测试,不会依赖本地文件是一个很好的单测方式

1.3.10 基准测试

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

其实基准测试就是测试一段程序的运行性能及耗费CPU的程度,该测试可以在开发遇到一直不能解决的问题的时候做性能分析

1.3.11 基准测试-例子

创建10台服务器,每次随机执行一个服务器

var ServerIndex [10]int

func InitServerIndenx() {
    for i := 0; i < 10; i++ {
        ServerIndex[i] = i + 100
    }
}

func Select() int {
    return ServerIndex[rand.Intn(10)]
}

1.3.12 基准测试-运行

测试主体,实现方式分为串行和并行

func BenchmarkSelect(b *testing.B) {
    InitServerIndex()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Select()
    }
}
func BenchmarkSelectParallel(b *testing.B) {
    InitServerIndex()
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            FastSelect()
        }
    } 
}
func FastSelect() int {
    return ServerIndex[fastrand.Intn(10)]
}

以上我使用了fastrand包,是一个高性能随机数方法,性能可以提升百倍,但牺牲了一定的数列一致性

1.4 项目实践

1.4.1 需求背景

2023-01-17-01-20-42-image.png

1.4.2 需求描述

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

1.4.3 分层结构

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

1.4.4 组件工具

  • Gin 高性能go web框架

    github.com/gin-gonic/g…

  • Go Mod

    go mod init(初始化管理配置文件)

    go get(下载gin依赖)

1.4.5 Repository-index

实现查询的方式,是使用索引得到内存Map

  • 创建索引
var (
    topicIndexMap map[int64]*Topic
    postIndexMap map[int64][]*Post
)
  • 初始化话题数据索引
func initTopicIndexMap(filePath string) error {
    open, err := os.Open(filePath + "topic ")
    if err != nil {
        return err  
    }
    scanner := bufio.NewScanner(open)
    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
        }
    topicTmpMap[topic.Id]= &topic
    }
    topicIndexMap = topicTmpMap
    return nil
}

1.4.6 Repository-查询

  • 通过索引ID返回话题数据
type Topic struct {
    Id int64 `json: "id"`
    Title string `json: "title"`
    Content string `json : "content"`
    CreateTime int64 `json : "create_time"`   
}
type TopicDao struct {
var (
    topicDao *TopicDaotopic
    0nce sync.Once
func NewTopicDaoInstanceo() *TopicDao {
    topicOnce.Do(
        func() {
            topicDao = &TopicDao{}      
        })
    return topicDao
}
func (*TopicDao) QueryTopicById(id int64) *Topic {
    return topicIndexMap[id] 
}

1.4.7 Service

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

首先是参数校验阶段,需要如下的代码流程编排

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
}

其次是准备数据阶段,是并行处理的,代码如下

func (f *QueryPageInfoFlow) prepareInfo() error {
    //获取topic信息
    var wg sync.WaitGroupwg.Add(2)
    go func() {...}()
    //获取post列表
    go func() {...}()
    wg.Wait(
    return nil
}

1.4.8 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{...}
    }
    pageInfo,err := service.QueryPageInfo(topicId)
    if err != nil {
        return &PageData{...}
    }
    return &PageData{...}

}

1.4.9 Router

  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务

下面由一段代码来实现以上四点

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