这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
一.并发
并发与并行
- 并发:多个线程在一个CPU上跑
- 并行:多个线程在多个CPU上上运行
二.协程(Goroutine)
协程:相当于一个一个的函数,在一个线程上运行,栈kb级别的
使用
go 调用
可以调用匿名内部函数作为协程的启用
通信
因为在一个线程中使用,访问速度非常快,直接通过通信共享内存
Channel信道
用来通信的工具
make(chan 类型,大小)
- 无缓冲
- 有缓冲
使用:
写入: channel <- 数据
读取: <- channel
注意:
- 写入信道后,一定要记得关闭信道 defer close
三.锁
在并发操作时,可能有多个非原子性操作,导致出局出现误差,所以我们要加锁进行安全
分为:
全局锁
无论读写全部加锁,效率低
使用:
var lock sync.Mutex
lock.lock()
lock.unlock()
读写锁
只读时不加锁提升效率,写时加锁
使用:
var lock sync.RWMutex
lock.RLock() // 读写锁
lock.RUnlock() // 读写锁
lock()
unlock()
WaitGroup
防止因为主线程结束而导致的线程提前结束
使用:
var wg sync.WaitGroup
wg.Add(几个子进程)
wg.Done() // 子协程结束后
wg.Wait() // 主进程等待
注意;
- Add一定是加在主进程中的
- Done在子协程中一定加 defer防止忘记计数
四.依赖管理必须依赖
复杂项目包含多个库,必须进行管理
GOPATH
缺点:
- 当两个函数或场景使用同一依赖的不同版本时,无法进行管理
Vendor
在GOPATH的基础上由多加了一层,需要几个版本的依赖就引入几个版本
缺点:
- 还是无法控制版本
- 更新多个版本可能会出现版本冲突
GoModel
- 通过go.mod进行管理
- 通过go.get获取下载
三要素
- 配置文件:go mod
版本定义分为:
-
- 语义化:V1.1.1
- 基于commit
常见版本:
-
- indirect 直接依赖简介依赖
- incompatible
- 中心仓库依赖管理:Proxy
控制下载的版本不会因为多个源而产生冲突
- 本地工具:go get
-
- go get
-
- go mod
五.测试
为了避免损失
回归测试
集成测试
单元测试
输入与输出是否一致
规则:
- 测试文件以_test.go结尾
- 测试函数以TestXxx(*testing.T)
- 初始化逻辑放在TestMain中
覆盖率:
不仅要测试到正常运行的情况,也要考虑到可能发生的错误情况
- 一般覆盖率50% ~ 69%,较好则高贵80%
- 测试分支相互独立,覆盖全面
- 测试单元粒度足够小,函数单一职责
基准测试
随机选择不同的服务器进行测试
六.实战项目
需求
通过输入话题的Id来展示话题以及伏安主话题的用户
业务逻辑
整体逻辑
分为数据层、逻辑层、视图控制层
- 数据层用于对实体进行增删改茶
- 逻辑层用于业务的具体处理
- 视图层(控制层) 用于对外暴露接口
如图:
数据层
整体逻辑如图:
- 为了实现内存节省,使用单例设计模式。
// 用来创建单例模式的
var (
postDao *PostDao
postOnce sync.Once // 单例模式创建
)
// NewPostDaoInstance 对外暴露创建实例的接口,返回值是一个无参构造的对象
func NewPostDaoInstance() *PostDao {
postOnce.Do(
func() {
postDao = &PostDao{}
})
return postDao
}
- 为了提高对数据的搜索效率,建立了以topicId为索引的map集合。
func initPostIndexMap(filePath string) error {
open, err := os.Open(filePath + "post")
if err != nil {
return err
}
scanner := bufio.NewScanner(open)
postTmpMap := make(map[int64][]*Post)
for scanner.Scan() {
text := scanner.Text()
var post Post
if err := json.Unmarshal([]byte(text), &post); err != nil {
return err
}
posts, ok := postTmpMap[post.ParentId]
if !ok {
postTmpMap[post.ParentId] = []*Post{&post}
continue
}
posts = append(posts, &post)
postTmpMap[post.ParentId] = posts
}
postIndexMap = postTmpMap
return nil
}
逻辑层
整体逻辑如下图:
主要是对数据的校验。返回一个话题和用户类的集合的返回体
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
func (f *QueryPageInfoFlow) checkParam() error {
if f.topicId <= 0 {
return errors.New("topic id must be larger than 0")
}
return nil
}
// 获取topicId 拿到id后查询关注id的用户
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
}
视图层
创建一个全局返回体,并调用service层的代码
// PageData 封装全局返回
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{
Code: -1,
Msg: err.Error(),
}
}
// 调用service中的方法
pageInfo, err := service.QueryPageInfo(topicId)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}