这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。
前言
青训营上课的第二天,主要学习了Go并发编程Goroutine,依赖管理go moudle,Go测试testing,以及项目实战。
Go并发
a.并发和并行的区别
并发,指的是多个事情,在同一时间段内同时发生了。并行,指的是多个事情,在同一时间点上同时发生了。两者的区别就在于多个线程是同一时间点发生,还是同一时间段发生。
b.Goroutine
注意到,
协程处于用户态,轻量级的线程,可以在用户态进行管理,栈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提倡通过通信共享内存即通道来通信。
1.Channel
下面以生产者和消费者来介绍通道channel来实现通信。
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锁来保证并发安全。
这里采用了time.Sleep()的方式实现主线程等待,可以通过sync.WaitGroup来完美替换。
Go依赖管理
a.背景
学会站在巨人的肩膀上做事,利用前人开发好的第三方组件或工具来提升自己的研发效率。在实际的项目开发中,不可能基于标准库从0-1开始造轮子,我们需要把更多的精力注入业务逻辑中,其他的组件如日志、框架等可以以依赖包的方式引入。
b.Go依赖管理演进
GOPATH --reason-不同项目将依赖版本不同 > Go Vender --reason-控制依赖库的版本不同 > Go Module
1、GOPATH
关键点:项目代码直接依赖src下的代码,把所有依赖的源代码都放在src下,通过go get下载最新版本的包。关键问题如下:
注:PA和PB同时依赖src的源码,就不能同时构建成功,当Pkg V2版本不兼容Pkg V1版本时,也即Pkg V2版本中没有函数A。
2、GO Vender
给每个项目引用一份依赖的副本go vendor,从而解决多个项目需要同一个package依赖冲突问题。
注:先从go vender中去获取依赖,没有就去gopath中获取。
问题所在:项目直接依赖的package确实不同,但是间接依赖的package却有不同的版本,而且两版本不兼容,就会项目构建不成功。
关键问题:vender也是依赖项目源码,而不能清楚标识依赖package的版本是多少。
3、Go Module
通过go.mod文件管理依赖包版本
通过go get/go mod指令工具管理依赖包
go Module可以定义版本规则和管理项目依赖关系。
4、依赖管理三要素
1-配置文件,描述依赖 go.mod
2-中心仓库管理依赖库 Proxy
配置环境变量GOPROXY,可选择阿里云镜像,和Go Proxy专业的镜像网站。
GOPROXY = "goproxy.cn,https://mirrors.al…"
注:经典模式:先P1,找不到P2,最后回到官方指定网站下载。类似于各级缓存 -》数据库寻找(终点)
3-本地工具 go get/mod
5、工具go get/mod
注:1.16默认开启,不用go init
Go测试
测试就是生命,事故的发生会有很多损失。
回归测试:测试产品功能。
集成测试:对接口进行集成测试。
单元测试:面对测试开发阶段,开发者对单独的函数模块进行测试。
a.单元测试
1、预期和实际结果是否相符。
2、测试规则:
func TestMain(m *testing.M) {
//测试前:数据装载、配置初始化等前置工作。
//跑pkg下的所有单测
code := m.Run()
//测试后:释放资源等收尾工作
os.Exit(code)
}
3、代码覆盖率
4、单元测试tips
1-一般覆盖率:50-60%,较高的覆盖率80%+ 2-测试分支相互独立且全面覆盖。 3-测试单元粒度足够小,这就要求函数单一职责,函数短且专一。
b.Mock测试
由于强依赖问题,为了保证幂等性(每次一样)和稳定性(单元测试相互隔离,任何时间任何函数能独立的运行),采用Mock机制,为函数打桩,以替换文件内容为例,可以不读文件,而是每次对初始文件内容进行测试。
c.基准测试(对代码进行资源消耗分析)
优化代码,需要对当前代码分析。
内置的测试框架提供了基准测试的能力。
实战
1、背景
2、需求描述
3、用例描述
4、ER图
5、分层结构
6、组件工具
7、Repository
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层,处理成相应的数据。
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
}
9、Controller
10、设置路由Router
11、运行
总结
1)Gorotine
2)Go Module
3)Testing