青训营后端第二天 | 青训营笔记

55 阅读5分钟

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

第二天

Go语言进阶与依赖管理

并发编程

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

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

Go可以充分发挥多核

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

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

func hello(i int) {
    println("hello goroutine : " + fmt.Sprint(i))
}
​
func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {  // 函数前加 go 表示创建一个协程
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

协程之间通信

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

channel

创建

make(chan 元素类型,[缓冲区大小])
·无缓冲通道      make(chan int)
·有缓冲通道      make(chan int, 2)  相当于一个队列

image-20230116141233392

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() {     // dest计算src输入进来数字的平方
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
    for i := range dest {
        //复杂操作
        println(i)
    }
}

对变量执行2000次的+1操作,5个协程并发执行

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()  // 开5个协程
    }
    time.Sleep(time.Second)
    println("WithoutLock:", x)
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("WithLock:", x)
}

加锁和不加锁的结果对比:不加锁会产生并发安全的问题

image-20230116142548984

WaitGroup计数器

image-20230116142825587

协程开启就+1,执行结束-1,主协程阻塞指导计数器为0

func ManyGoWait() {
    var wg sync.WaitGroup
    wg.Add(5)  // 计数器+5
    for i := 0; i < 5; i++ {
        go func(j int) { // 开启5个协程
            defer wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}

依赖管理

站在巨人的肩膀上

GOPATH

√环境变量 $GOPATH

bin pkg src三个包

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

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

弊端

因为所有依赖都在一个包下面无法进行多版本控制

image-20230116143740447

Go Vendor

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

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

image-20230116144009765

弊端

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

image-20230116144211787

Go Module

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

依赖管理三要素

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

image-20230116144640040

依赖标识:【Module Path】[Version/Pseudo-version]

version定义

  • 语义化版本

MAJOR.{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

indirect非直接依赖关键字

image-20230116145334393

incompatible关键字

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

image-20230116145542231

会选择最低的兼容版本

image-20230116145901154

依赖分发-回源

image-20230116150051364

依赖分发-Proxy

image-20230116150146518

依赖分发-变量 GOPROXY

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

上面是站点的URL列表,"direct"代表源站,如果Proxy1中没有就去Proxy2中去找,再没有就去源站direct去找

image-20230116150346921

工具- go get

image-20230116150554610

工具- go mod

image-20230116150701363

Go语言工程实践之测试

image-20230116160154228

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

单元测试 - 规则

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

单元测试 - 覆盖率

后面加个cover,保证测试用例的覆盖率

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

单元测试 - 依赖

幂等: 多次运行的结果是一样的

稳定: 测试分支相互独立

image-20230116161507301

如果单独在数据库环境下测试可能出现错误,因为可能用到其他的例如缓存Cache

单元测试 - mock

开源mokey是一个开源的mock测试包

  • 为一个函数打桩(就是用A函数替换B函数)
  • 为一个方法打桩

桩,或称桩代码,是指用来代替关联代码或者未实现代码的代码。如果函数B用B1来代替,那么,B称为原函数,B1称为桩函数。打桩就是编写或生成桩代码。

打桩的目的

打桩的目的主要有:隔离、补齐、控制。

  • 隔离是指将测试任务从产品项目中分离出来,使之能够独立编译、链接,并独立运行。隔离的基本方法就是打桩,将测试任务之外的,并且与测试任务相关的代码,用桩来代替,从而实现分离测试任务。例如函数A调用了函数B,函数B又调用了函数C和D,如果函数B用桩来代替,函数A就可以完全割断与函数C和D的关系(隔离了A和C、D,而不是隔离A和B)。
  • 补齐是指用桩来代替未实现的代码,例如,函数A调用了函数B,而函数B由其他程序员编写,且未实现,那么,可以用桩来代替函数B,使函数A能够运行并测试。补齐在并行开发中很常用。
  • 控制是指在测试时,人为设定相关代码的行为,使之符合测试需求。例如:

Patch函数:target为要替换的原函数,replacement为桩

image-20230116172201315

image-20230116173128523

单元测试 - 文件处理

基准测试

优化代码,提高性能

项目实践

需求设计

社区话题页面

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

话题topic

type Topic struct {
   Id         int64  `json:"id"`
   Title      string `json:"title"`
   Content    string `json:"content"`
   CreateTime int64  `json:"create_time"`
}

帖子Post

type Post struct {
   Id         int64  `json:"id"`
   ParentId   int64  `json:"parent_id"`
   Content    string `json:"content"`
   CreateTime int64  `json:"create_time"`
}

分层结构

image-20230116174156824

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

组件工具

两个索引

var (
   topicIndexMap map[int64]*Topic
   postIndexMap  map[int64][]*Post
)

初始化Topic话题数据索引

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  // 得到的结构体数据放入Map索引中
   }
   topicIndexMap = topicTmpMap
   return nil
}

根据得到的索引查询, 索引:话题ID 数据:话题

var (
    topicDao  *TopicDao
    topicOnce sync.Once   // sync.once 类似于单例模式,只查询一次,防止重复查询
)
func NewTopicDaoInstance() *TopicDao {
    topicOnce.Do(
        func() {
            topicDao = &TopicDao{}
        })
    return topicDao
}
func (*TopicDao) QueryTopicById(id int64) *Topic {
    return topicIndexMap[id]
}

Service

实体

topic   *repository.Topic
posts   []*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.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
}

Controller

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

Router

通过Gin搭建web框架