课程笔记 - 工程进阶 | 青训营笔记

239 阅读1分钟

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


工程进阶

携程: goroutine

运行以下代码,观察输出可知 i 恒为 5,而 j 的输出不定,在 0-4 内变化

var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
   go func(j int) {
      defer wg.Done()
      fmt.Println(i, j)
   }(i)
}
wg.Wait()

观察:当循环次数增加至 500 时,绝大部分 i 的输出为 500,有小于边界值的输出出现 思考:当 i 为个数级时,携程总在循环结束后执行,说明创建携程需要花费一定的时间;当 i 为百级时,携程在循环中执行,说明 goroutine 的创建资源消耗小于常规语言中线程创建的消耗(如 java,便无法复现

chan

golang 提倡通过通信(chan)实现共享内存,而不是通过共享内存实现通信

// channel 保证了并发的安全(顺序)性
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)
}

在多线程程序中,锁是确保线程安全的实现中不可或缺的一环,golang 中常用的锁为 sync.Mutex

x = 0
for i := 0; i < 5; i++ {
   go addWithoutLock()
}
//time.Sleep(time.Second),不优雅
println("Without lock", x) // < 10000
x = 0
for i := 0; i < 5; i++ {
   go addWithLock() // = 10000
}
time.Sleep(time.Second)
println("With lock", x)
func addWithLock() {
   for i := 0; i < 2000; i++ {
      lock.Lock()
      x += 1
      lock.Unlock()
   }
}
func addWithoutLock() {
   for i := 0; i < 2000; i++ {
      x += 1
   }
}

思考:addWithoutLock 中算得 x 数值偏小,其原因为在多携程下各携程间感知变量更新不及时所致,常用解决方案为加锁,如本例中的 sync.Mutex,亦或是 CAS 等

工程结构与包管理系统的演变

origin

在终端输入以下代码,查看当前 golang 项目的工作目录结构

[user@server $GOPATH]# du -d 1

经典的 golang 项目应该具备以下三个目录

  • bin 编译的二进制文件
  • pkg 编译中间产物 用于加速编译
  • src 源码

传统项目结构有一个弊端,就是无法实现依赖的版本控制

Go Vendor

为了解决上述问题,go vendor 出现了。在项目创建时会额外生成 vendor 目录用于存放依赖副本,由此查找依赖顺序变为 vendor -> GOPATH。但在实际使用时仍存在弊端:go直接依赖源码,无法区分依赖的版本,依赖的项目依赖两个不兼容版本的依赖,构建失败

Go Moudle

为了彻底解决而此类问题,golang 官方推出 go moudle 工具,其实现版本控制的三要素对应如下:

  • 配置文件 go.mod
  • 中心仓库 Proxy
  • 本地工具 go get/mod

在 go.mod 文件中,有许多标识符,其中:

  • indirect 间接依赖模块标识
  • /vN 主版本2+的模块会在路径后增加此兼容后缀
  • incompatible 标识主版本2+且没有go.mod文件的依赖

golang 官方同样规定了语义化版本的规则:

  • 正式版本 MAJOR.{MAJOR}.{MINOR}.${PATCH}
  • commit版本 vX.0.0-yyyymmddhhmmss-abcdefgh1234

测试

在 golang 中,测试文件需以 _test.go 结尾,随即便可使用 go test xx_test.go 进行测试。

一般来说,测试代码的主函数如下

func TestMain(m *testing.M) {
   // do init
   code := m.Run() // 执行全部测试
   // do close
   os.Exit(code)
}

单元测试

单元测试的函数名以 Test 开头,通常用来单独测试某一函数的功能:

func TestHelloIllTamer(t *testing.T) {
   output := HelloIllTamer()
   expectOutput := "IllTamer"
   //if output != expectOutput {
   // t.Errorf("Not match: expected %s but %s", expectOutput, output)
   //}
   assert.Equal(t, expectOutput, output)
}

mock 测试

mock 在英文中是伪造的意思,他的作用在于利用反射机制替换某些如读取文件之类的函数,达到测试不受影响且幂等的结果:

func TestHelloIllTamerMock(t *testing.T) {
   monkey.Patch(DoHelloIllTamer, func() string {
      return "IllTamer"
   })
   defer monkey.Unpatch(DoHelloIllTamer)
   output := HelloIllTamer()
   assert.Equal(t, "IllTamer", output)
}

基准测试

基准测试是指专门用于测试函数性能的测试项,以 Benchmark 开头。使用 gobench xxx_test.go 运行测试代码中的基准测试

// 串行测试
// BenchmarkRandom-16              191787325           6.262 ns/op
func BenchmarkRandom(b *testing.B) {
   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      Random(10)
   }
}

// 并行测试(rand函数内置锁,并发性能下降)
// BenchmarkRandomParallel-16      19625052           53.18 ns/op
func BenchmarkRandomParallel(b *testing.B) {
   b.ResetTimer()
   b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
         Random(10)
      }
   })
}

作业

作业要求在原有查询 topic 和 post 的基础上,实现对 post 的新增支持。本人实现的完整代码如下

package main

import (
   "bufio"
   "encoding/json"
   "fmt"
   "github.com/gin-gonic/gin"
   "os"
   "strconv"
   "sync"
   "time"
)

// 回帖后端实战
func main() {
   if err := Init("./resource/04/"); err != nil {
      fmt.Println(err)
      os.Exit(-1)
   }
   engine := gin.Default()
   engine.GET("/community/page/get/:id", func(context *gin.Context) {
      topicId := context.Param("id")
      data := QueryPageInfo(topicId)
      context.JSON(200, data)
   })
   engine.POST("/community/page/add/:parent_id", func(context *gin.Context) {
      parentId := context.Param("parent_id")
      content := context.Param("content")
      data := AddPost(parentId, content)
      context.JSON(200, data)
   })
   err := engine.Run()
   if err != nil {
      return
   }
}

var (
   initParam *InitParam
   initOnce  sync.Once
)

type InitParam struct {
   filePath string
}

func NewInitParamInstance() *InitParam {
   initOnce.Do(func() {
      initParam = &InitParam{}
   })
   return initParam
}

func Init(filePath string) error {
   NewInitParamInstance().filePath = filePath
   err := initTopicIndexMap(filePath)
   if err != nil {
      return err
   }
   err = initPostIndexMap(filePath)
   if err != nil {
      return err
   }
   return nil
}

// --------------------------------------------------
// controller

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{1, "topicId 解析失败", err.Error()}
   }
   pageInfo, err := DoQueryPageInfo(topicId)
   if err != nil {
      return &PageData{2, "查询失败", err.Error()}
   }
   return &PageData{0, "查询成功", pageInfo}
}

func AddPost(parentIdStr string, content string) *PageData {
   if len(content) == 0 {
      return &PageData{1, "content can not be null", nil}
   }
   parentId, err := strconv.ParseInt(parentIdStr, 10, 64)
   if err != nil {
      return &PageData{1, "parentId 解析失败", err.Error()}
   }
   newId, err := DoAddPost(parentId, content, time.Now().Unix())
   if err != nil {
      return &PageData{2, "Add failed", err.Error()}
   }
   return &PageData{0, "Success", newId}
}

// --------------------------------------------------
// service

type PageInfo struct {
   Topic    *Topic
   PostList []*Post
}

type QueryPageInfoFlow struct {
   topicId  int64
   pageInfo *PageInfo

   topic *Topic
   posts []*Post
}

type AddPostFlow struct {
   parentId   int64
   content    string
   createTime int64

   postId int64
}

func DoQueryPageInfo(topicId int64) (*PageInfo, error) {
   return NewQueryPageInfoFlow(topicId).Do()
}

func DoAddPost(parentId int64, content string, createTime int64) (int64, error) {
   return NewAddPostFlow(parentId, content, createTime).Do()
}

func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
   return &QueryPageInfoFlow{
      topicId: topId,
   }
}

func NewAddPostFlow(parentId int64, content string, createTime int64) *AddPostFlow {
   return &AddPostFlow{
      parentId:   parentId,
      content:    content,
      createTime: createTime,
   }
}

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) checkParam() error {
   // do nothing
   return nil
}

func (f *QueryPageInfoFlow) prepareInfo() error {
   var wg sync.WaitGroup
   wg.Add(2)
   // 获取 topic 信息
   go func() {
      defer wg.Done()
      topic := NewTopicDaoInstance().QueryTopicById(f.topicId)
      f.topic = topic
   }()
   // 获取 post 列表
   go func() {
      defer wg.Done()
      posts := NewPostDaoInstance().QueryPostListById(f.topicId)
      f.posts = posts
   }()
   wg.Wait()
   return nil
}

func (f *QueryPageInfoFlow) packPageInfo() error {
   if f.topic == nil || f.posts == nil {
      return fmt.Errorf("not found")
   }
   f.pageInfo = &PageInfo{
      Topic:    f.topic,
      PostList: f.posts,
   }
   return nil
}

func (f *AddPostFlow) Do() (int64, error) {
   if err := f.checkParam(); err != nil {
      return -1, err
   }
   if err := f.preparePostId(); err != nil {
      return -1, err
   }
   if err := f.addPost(); err != nil {
      return -1, err
   }
   return f.postId, nil
}

func (f *AddPostFlow) checkParam() error {
   topic := NewTopicDaoInstance().QueryTopicById(f.parentId)
   if topic == nil {
      return fmt.Errorf("can't find topic %d", f.parentId)
   }
   return nil
}

func (f *AddPostFlow) preparePostId() error {
   lock.Lock()
   f.postId = NewPostDaoInstance().GetAndRefreshPostId()
   lock.Unlock()
   return nil
}

// preparePostId has attached lock, so it's unnecessary to lock this
func (f *AddPostFlow) addPost() error {
   post := Post{f.postId, f.parentId, f.content, f.createTime}
   err := NewPostDaoInstance().AddPost(&post)
   if err != nil {
      return err
   }
   lock.Lock()
   // 需确保操作的原子性
   err = NewPostDaoInstance().RefreshPostMap(&post)
   lock.Unlock()
   if err != nil {
      return err
   }
   return nil
}

// --------------------------------------------------
// repository

type TopicDao struct {
}

type PostDao struct {
   idIndex int64
}

var (
   topicDao  *TopicDao
   topicOnce sync.Once
   lock      sync.Mutex
)

var (
   postDao  *PostDao
   postOnce sync.Once
)

func NewTopicDaoInstance() *TopicDao {
   topicOnce.Do(func() {
      topicDao = &TopicDao{}
   })
   return topicDao
}

func NewPostDaoInstance() *PostDao {
   postOnce.Do(func() {
      postDao = &PostDao{}
   })
   return postDao
}

// QueryTopicById 根据 topicId 查询话题
func (*TopicDao) QueryTopicById(topicId int64) *Topic {
   return topicIndexMap[topicId]
}

// QueryPostListById 根据 topicId 查询帖子列表
func (*PostDao) QueryPostListById(topicId int64) []*Post {
   return postIndexMap[topicId]
}

func (dao *PostDao) GetAndRefreshPostId() int64 {
   dao.idIndex++
   return dao.idIndex
}

func (*PostDao) AddPost(post *Post) error {
   filePath := NewInitParamInstance().filePath
   open, err := os.Open(filePath + "post")
   if err != nil {
      return err
   }
   writer := bufio.NewWriter(open)
   buf, err := json.Marshal(post)
   _, err = writer.Write(buf)
   if err != nil {
      return err
   }
   return nil
}

func (*PostDao) RefreshPostMap(post *Post) error {
   posts := postIndexMap[post.ParentId]
   posts = append(posts, post)
   postIndexMap[post.ParentId] = posts
   return nil
}

type Topic struct {
   Id         int64  `json:"id"`
   Title      string `json:"title"`
   Content    string `json:"content"`
   CreateTime int64  `json:"create_time"`
}

type Post struct {
   Id         int64  `json:"id"`
   ParentId   int64  `json:"parent_id"`
   Content    string `json:"content"`
   CreateTime int64  `json:"create_time"`
}

var (
   // topicId -> Topic
   topicIndexMap map[int64]*Topic
   // parentId -> []Post
   postIndexMap map[int64][]*Post
)

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
      err := json.Unmarshal([]byte(text), &topic)
      if err != nil {
         return err
      }
      topicTmpMap[topic.Id] = &topic
   }
   topicIndexMap = topicTmpMap
   return nil
}

func initPostIndexMap(filePath string) error {
   open, err := os.Open(filePath + "post")
   if err != nil {
      return err
   }
   postTmpMap := make(map[int64][]*Post)
   // 逐行读写
   scanner := bufio.NewScanner(open)
   instance := NewPostDaoInstance()
   for scanner.Scan() {
      text := scanner.Text()
      var post Post
      err := json.Unmarshal([]byte(text), &post)
      if err != nil {
         return err
      }
      posts, ok := postTmpMap[post.ParentId]
      if !ok { // key 不存在
         postTmpMap[post.ParentId] = []*Post{}
      }
      posts = append(posts, &post)
      postTmpMap[post.ParentId] = posts
      if post.Id > instance.idIndex {
         instance.idIndex = post.Id
      }
   }
   postIndexMap = postTmpMap
   return nil
}