5.8_Go上手实践 | 青训营笔记

147 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。

篇幅目录

  • Go并发编程入门
  • 依赖管理
  • 项目实战

Go并发编程入门

这一节主要涉及到Go语言中协程Goroutine、通道Channel、锁Lock和线程同步WaitGroup相关的入门学习。

协程Goroutine

学习过操作系统这门课后,对进程和线程两个概念应该都比较熟悉。进程的出现是为了更充分的利用CPU的资源使得能够并发执行任务。线程则是为了解决并发执行任务过程中,切换进程的开销过大的问题。但线程仍存在问题:(1)线程的切换仍然需要经由用户态->内核态->用户态的转换;(2)内核的调度对象是进程,用户线程发生IO阻塞时,整个进程都会被阻塞。

协程可以简单理解为轻量级线程,用户自己实现对协程的切换调用,不用经过内核态的转换,因此速度更快。Go语言天然支持协程,使用go关键字创建协程。

go func(params ...types){
    ...
}(args)

管道Channel

Go语言中有两种方式实现通信

  • 通过通信共享内存(Channel)
  • 通过共享内存实现通信(Lock)

CSP.jpg

用共享内存的方式,会对临界区加锁,可能发生数据静态的问题,影响性能,所以推荐采用Channel的方式。

Go语言使用make(chan type, [size])创建通道

  1. 无缓冲通道 make(chan int):通道两端必须同时准备好发送和接受,才能实现通信。
  2. 有缓存通道make(chan int, 2):缓冲区大小为2,中途会发生阻塞。

Lock

sync.mutex感觉上与Java中的API层面实现的ReentrantLock类似。都需要手动进行加锁和解锁的过程。以计数器为例,不加锁会产生错误的结果。另外在基准测试下,不加锁基本都会比加锁更快,而自己手动使用time包统计的时候,加锁反而更快,这点令我很费解,没找到答案,不过整体上应该还是以基准测试为准。

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
   }
}

sync_bench.jpg


WaitGroup

WaitGroup是为了解决并发任务的等待问题而出现的,假设有几个并行的Goroutine,在他们都完成后,我们将继续进行后续的操作,手动轮询和等待的方式明显是不切合实际的,WaitGroup是一个很好的工具,主体上提供3个函数,供我们完成,在命令行词典中,也做了基本的实践。 waitgroup.png

#依赖管理

GOPATH

GOPATH是Go语言支持的一个环境变量,但GOPATH无法实现版本的控制,始终获取的是包最新的版本,不利于我们对老版本的兼容问题。


GOMODULE

GOMODULE是Go1.11引进的依赖管理系统,使用go.mod文件进行版本控制,我们需要在go env手动开启,并配置GOPROXY模块代理。

$ go env -w GO111MODULE=on 
$ go env -w GOPROXY=https://goproxy.cn,direct

项目实战及作业

要求

完成一个简易的社区话题页面

  • 展示话题(标题,文字描述)和回帖列表
  • 暂不考虑前端页面
  • 话题和回帖数据用文件存储

课后作业

  • 支持发布帖子
  • 本地id唯一
  • Append文件,更新索引,注意Map并发问题。

课后作业主要模块

基本功能的代码可以参考github.com/Moonlight-Z…

课后新增功能

util

实现id唯一生成,这部分参考了Snowflake算法,下面列举了实现的主体数据结构。

const (
   baseValue int64 = -1
   startTime int64 = 1651334400 //开始时间戳 2022-5-1 00:00:00
   //Bits
   workerIdBits     uint = 5  //机器id位数
   datacenterIdBits uint = 5  //数据中心id位数
   sequenceBits     uint = 12 //序列位数
   //Max || Mask
   maxWorkerId     int64 = baseValue ^ (baseValue << workerIdBits)     //最大机器id位数
   maxDatacenterId int64 = baseValue ^ (baseValue << datacenterIdBits) //最大数据中心id
   sequenceMask    int64 = baseValue ^ (baseValue << sequenceBits)     //序列掩码 12位 4095
   //shift
   workerIdLeftShift     uint = sequenceBits                             //机器id需要左移的位数
   datacenterIdLeftShift uint = workerIdLeftShift + workerIdBits         //数据中心左移位数
   timestampLeftShift    uint = datacenterIdLeftShift + datacenterIdBits //时间戳左移位数
)

type SnowflakeIdWorker struct {
   workerId      int64
   datacenterId  int64
   sequence      int64
   lastTimestamp int64 //上次生成id的timestamp
   idLock        *sync.Mutex
}

repository

追写post并用sync.RWMutex实现Map并发。

func (*PostDao) AddPost(post *Post) error {
   f, err := os.OpenFile("../data/post", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
   if err != nil {
      return err
   }
   defer f.Close()
   postJson, _ := json.Marshal(post)
   if _, err = f.WriteString("\n" + string(postJson)); err != nil {
      return err
   }
   //Map加锁
   rwMutex.Lock()
   postList, ok := postIndexMap[post.ParentId]
   if !ok {
      postIndexMap[post.ParentId] = []*Post{post}
   } else {
      postList = append(postList, post)
      postIndexMap[post.ParentId] = postList
   }
   rwMutex.Unlock()
   return nil
}

service

部分核心的代码。

//中间流
type PublishPostFlow struct {
   topicId int64
   content string
   postId  int64
}

func (p *PublishPostFlow) publish() error {
   post := &repository.Post{
      ParentId:   p.topicId,
      Content:    p.content,
      CreateTime: time.Now().UnixNano() / 1e6,
   }
   id, err := idWorker.NextId()
   if err != nil {
      return err
   }
   post.Id = id
   if err := repository.NewPostDaoInstance().AddPost(post); err != nil {
      return err
   }
   p.postId = post.Id
   return nil
}

controller

func PublishPost(topicIdStr, content string) *PageData {
   fmt.Println("topicIdStr: ", topicIdStr)
   topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
   if err != nil {
      return &PageData{
         Code: -1,
         Msg:  err.Error(),
      }
   }
   postId, err := service.PublishPost(topicId, content)
   if err != nil {
      return &PageData{
         Code: -1,
         Msg:  err.Error(),
      }
   }
   return &PageData{
      Code: 0,
      Msg:  "success",
      Data: map[string]int64{
         "post_id": postId,
      },
   }
}

main

r.POST("/community/post/publish", func(c *gin.Context) {
   topicId, _ := c.GetPostForm("topic_id")
   content, _ := c.GetPostForm("content")
   data := controller.PublishPost(topicId, content)
   c.JSON(200, data)
})

实现结果

在windows下会报'gcc' not found的错误,可以参考MinGW-w64 离线包安装方法配置。

报错.jpg

Apifox模拟结果如下图所示

apifox.jpg

数据库的版本有待后续实现。