Go工程实践 | 青训营笔记

205 阅读4分钟

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

前言

青训营上课的第二天,主要学习了Go并发编程Goroutine,依赖管理go moudle,Go测试testing,以及项目实战。

Go并发

a.并发和并行的区别

并发,指的是多个事情,在同一时间段内同时发生了。并行,指的是多个事情,在同一时间点上同时发生了。两者的区别就在于多个线程是同一时间点发生,还是同一时间段发生。

b.Goroutine

image.png 注意到,

协程处于用户态,轻量级的线程,可以在用户态进行管理,栈KB级别。

线程处理内核态,一个线程可以对应跑多个协程,它是系统宝贵的资源,栈MB级别。

例子:快速打印hello world 0 - 4,为了快速打印,所以开5个协程并发执行。当然这个快是广义的,本身多线程并不一定比单线程快,不详细阐述。

这里主要练习Goroutine的使用,展现Go开启一个协程有多方便,只需要在函数前加一个go关键字即可为一个函数创建一个协程。

func ManyGo() {
   var wg sync.WaitGroup
   for i := 0; i < 5; i++ {
      wg.Add(1)
      go func(j int) {
         defer wg.Done()
         hello(j)
      }(i)
   }
   wg.Wait()
}
hello world : 0
hello world : 2
hello world : 1
hello world : 3
hello world : 4
--- PASS: TestManyGo (0.00s)
PASS


Process finished with the exit code 0

b.communication

并发是多个协程之间的并发,有协程就会有协程通信和协程安全,这里主要介绍协程通信。

协程通信有常见的两种方式,消息队列和共享内存,即称通道和临界区(通过互斥量来对临界区进行加锁以获取操作权限)。Go提倡通过通信共享内存即通道来通信。

image.png

1.Channel

下面以生产者和消费者来介绍通道channel来实现通信。

image.png

package concurrence

import (
   "fmt"
   "sync"
)

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

/*
target--多协程:两个同步生产者,一个生产数字,一个根据数字生产数字的平方;一个消费者,打印这些数字的平方。
如何生产数字?生产数字较快,可以用无缓冲消息通信。
如何生产数字的平方?从无缓冲的消息中拿到数字,生产平方到有缓冲的消息队列中,毕竟平方数字生产的慢。
如何消费?把消息队列的信息取出。
*/
func produce1(src chan int, wg sync.WaitGroup) {
   wg.Add(1)
   defer wg.Done()

   defer close(src)
   for i := 1; i < 5; i++ {
      src <- i
   }
}
func produce2(src chan int, dest chan int, wg sync.WaitGroup) {
   wg.Add(1)
   defer wg.Done()

   defer close(dest)
   for val := range src {
      dest <- val * val
   }
}
func customer(dest chan int, wg sync.WaitGroup) {
   wg.Add(1)
   defer wg.Done()

   for val := range dest {
      fmt.Println(val)
   }
}
func producerAndCustomer() {
   var wg sync.WaitGroup

   src := make(chan int)
   go produce1(src, wg)

   dest := make(chan int, 3)
   go produce2(src, dest, wg)

   go customer(dest, wg)

   wg.Wait()
}
=== RUN   TestProducerAndCustomer
1
4
9
16
--- PASS: TestProducerAndCustomer (0.00s)
PASS

Debugger finished with the exit code 0

c.并发安全 Lock 与 Sync

Go也保持了通过共享内存来实现协程之间的通信,这就会涉及到多个协程操作同一块内存资源的情况。为了保证并发安全,可以通过Mutex锁来保证并发安全。

image.png 这里采用了time.Sleep()的方式实现主线程等待,可以通过sync.WaitGroup来完美替换。

image.png

Go依赖管理

a.背景

学会站在巨人的肩膀上做事,利用前人开发好的第三方组件或工具来提升自己的研发效率。在实际的项目开发中,不可能基于标准库从0-1开始造轮子,我们需要把更多的精力注入业务逻辑中,其他的组件如日志、框架等可以以依赖包的方式引入。

b.Go依赖管理演进

GOPATH --reason-不同项目将依赖版本不同 > Go Vender --reason-控制依赖库的版本不同 > Go Module

1、GOPATH

image.png 关键点:项目代码直接依赖src下的代码,把所有依赖的源代码都放在src下,通过go get下载最新版本的包。关键问题如下:

image.png 注:PA和PB同时依赖src的源码,就不能同时构建成功,当Pkg V2版本不兼容Pkg V1版本时,也即Pkg V2版本中没有函数A。

2、GO Vender

给每个项目引用一份依赖的副本go vendor,从而解决多个项目需要同一个package依赖冲突问题。

image.png 注:先从go vender中去获取依赖,没有就去gopath中获取。 问题所在:项目直接依赖的package确实不同,但是间接依赖的package却有不同的版本,而且两版本不兼容,就会项目构建不成功。 image.png 关键问题:vender也是依赖项目源码,而不能清楚标识依赖package的版本是多少。

3、Go Module

通过go.mod文件管理依赖包版本

通过go get/go mod指令工具管理依赖包

go Module可以定义版本规则和管理项目依赖关系。

4、依赖管理三要素

1-配置文件,描述依赖 go.mod

image.png

image.png

image.png

image.png

2-中心仓库管理依赖库 Proxy

配置环境变量GOPROXY,可选择阿里云镜像,和Go Proxy专业的镜像网站。

GOPROXY = "goproxy.cn,https://mirrors.al…"

image.png

image.png

image.png 注:经典模式:先P1,找不到P2,最后回到官方指定网站下载。类似于各级缓存 -》数据库寻找(终点) 3-本地工具 go get/mod

5、工具go get/mod

image.png

image.png 注:1.16默认开启,不用go init

Go测试

测试就是生命,事故的发生会有很多损失。

image.png 回归测试:测试产品功能。

集成测试:对接口进行集成测试。

单元测试:面对测试开发阶段,开发者对单独的函数模块进行测试。

a.单元测试

1、预期和实际结果是否相符。

image.png

2、测试规则:

image.png

func TestMain(m *testing.M) {
   //测试前:数据装载、配置初始化等前置工作。
   //跑pkg下的所有单测
   code := m.Run()
   //测试后:释放资源等收尾工作
   os.Exit(code)
}

3、代码覆盖率

image.png

4、单元测试tips

1-一般覆盖率:50-60%,较高的覆盖率80%+ 2-测试分支相互独立且全面覆盖。 3-测试单元粒度足够小,这就要求函数单一职责,函数短且专一。

b.Mock测试

image.png 由于强依赖问题,为了保证幂等性(每次一样)和稳定性(单元测试相互隔离,任何时间任何函数能独立的运行),采用Mock机制,为函数打桩,以替换文件内容为例,可以不读文件,而是每次对初始文件内容进行测试。

image.png

image.png

c.基准测试(对代码进行资源消耗分析)

优化代码,需要对当前代码分析。

内置的测试框架提供了基准测试的能力。

image.png

image.png

image.png

实战

1、背景

image.png

2、需求描述

image.png

3、用例描述

image.png

4、ER图

image.png

5、分层结构

image.png

6、组件工具

image.png

7、Repository

image.png

image.png

image.png Respoosity-查询,

package repository

import (
   "github.com/Moonlight-Zhao/go-project-example/util"
   "sync"
   "time"
)

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"`
}

func (Topic) TableName() string {
   return "topic"
}

type TopicDao struct {
}

var topicDao *TopicDao
var topicOnce sync.Once

func NewTopicDaoInstance() *TopicDao {
//单列模式,减少内存浪费。
   topicOnce.Do(
      func() {
         topicDao = &TopicDao{}
      })
   return topicDao
}

func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {
   var topic Topic
   err := db.Where("id = ?", id).Find(&topic).Error
   if err != nil {
      util.Logger.Error("find topic by id err:" + err.Error())
      return nil, err
   }
   return &topic, nil
}

8、Service

Service层调用Repository层,处理成相应的数据。

image.png

func (f *PublishPostFlow) Do() (int64, error) {
   if err := f.checkParam(); err != nil {
      return 0, err
   }
   if err := f.publish(); err != nil {
      return 0, err
   }
   return f.postId, nil
}

image.png

9、Controller

image.png

10、设置路由Router

image.png

11、运行

image.png

总结

1)Gorotine

2)Go Module

3)Testing