后端基础_02 | 青训营笔记

82 阅读3分钟

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

一.并发

并发与并行

  • 并发:多个线程在一个CPU上跑
  • 并行:多个线程在多个CPU上上运行

二.协程(Goroutine)

协程:相当于一个一个的函数,在一个线程上运行,栈kb级别的

使用

go 调用

可以调用匿名内部函数作为协程的启用

通信

因为在一个线程中使用,访问速度非常快,直接通过通信共享内存

Channel信道

用来通信的工具

make(chan 类型,大小)

  • 无缓冲
  • 有缓冲

使用

写入: channel <- 数据

读取: <- channel

注意:

  1. 写入信道后,一定要记得关闭信道 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() // 主进程等待

注意

  1. Add一定是加在主进程中的
  2. Done在子协程中一定加 defer防止忘记计数

四.依赖管理必须依赖

复杂项目包含多个库,必须进行管理

GOPATH

缺点:

  1. 当两个函数或场景使用同一依赖的不同版本时,无法进行管理

Vendor

在GOPATH的基础上由多加了一层,需要几个版本的依赖就引入几个版本

缺点:

  1. 还是无法控制版本
  2. 更新多个版本可能会出现版本冲突

GoModel

  • 通过go.mod进行管理
  • 通过go.get获取下载

三要素

  • 配置文件:go mod

版本定义分为:

    1. 语义化:V1.1.1
    2. 基于commit

常见版本:

    1. indirect 直接依赖简介依赖
    2. incompatible
  • 中心仓库依赖管理:Proxy

控制下载的版本不会因为多个源而产生冲突

  • 本地工具:go get
    • go get

    • go mod

五.测试

为了避免损失

回归测试

集成测试

单元测试

输入与输出是否一致

规则:

  1. 测试文件以_test.go结尾
  2. 测试函数以TestXxx(*testing.T)
  3. 初始化逻辑放在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,
	}

}