这是我参与「第五届青训营」伴学笔记创作活动的第 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) 相当于一个队列
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)
}
加锁和不加锁的结果对比:不加锁会产生并发安全的问题
WaitGroup计数器
协程开启就+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目录下
弊端
因为所有依赖都在一个包下面无法进行多版本控制
Go Vendor
- 项目目录下新增
vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:
vendor=> GOPATH vendor下没有才去GOPATH下寻找
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
弊端
- 无法控制依赖的版本
- 更新项目有可能出现依赖冲突,导致编译出错
Go Module
- 通过
go.mod文件 管理依赖包版本 - 通过
go get/go mod指令工具 管理依赖包
依赖管理三要素
- 配置文件,描述依赖
go.mod - 中心仓库管理依赖库
Proxy - 本地工具
go get/mod
依赖标识:【Module Path】[Version/Pseudo-version]
version定义
- 语义化版本
{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非直接依赖关键字
incompatible关键字
- 主版本2+ 模块会在模块路径增加/vN后缀
- 对于没有
go.mod文件并且主版本为 2+ 的依赖,会 +incompatible关键字
会选择最低的兼容版本
依赖分发-回源
依赖分发-Proxy
依赖分发-变量 GOPROXY
GOPROXY="proxy1.cn, proxy2.cn, direct"
上面是站点的URL列表,"direct"代表源站,如果Proxy1中没有就去Proxy2中去找,再没有就去源站direct去找
工具- go get
工具- go mod
Go语言工程实践之测试
从上到下,覆盖率逐层变大,成本却逐层降低
单元测试 - 规则
- 所有测试文件以
_test.go结尾 - 测试函数命名
func TestXxx(*testing.T) - 初始化逻辑放到
TestMain中
单元测试 - 覆盖率
后面加个cover,保证测试用例的覆盖率
go test judgment_test.go judgment.go --cover
- 一般覆盖率:50% ~ 60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一负责
单元测试 - 依赖
幂等: 多次运行的结果是一样的
稳定: 测试分支相互独立
如果单独在数据库环境下测试可能出现错误,因为可能用到其他的例如缓存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为桩
单元测试 - 文件处理
基准测试
优化代码,提高性能
项目实践
需求设计
社区话题页面
| 展示话题(标题,文字描述 )和回帖列表 |
|---|
| 暂不考虑前端页面实现,仅仅实现一个本地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"`
}
分层结构
- 数据层:Model,外部数据的增删改查
- 逻辑层:Entity,处理核心业务逻辑输出
- 视图层:View,处理和外部的交互逻辑
组件工具
-
Go Mod
- go mod init
- go get gopkg.in/gin-gonic/gin.v1@v1.3.0
两个索引
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框架