这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。
篇幅目录
- Go并发编程入门
- 依赖管理
- 项目实战
Go并发编程入门
这一节主要涉及到Go语言中协程Goroutine、通道Channel、锁Lock和线程同步WaitGroup相关的入门学习。
协程Goroutine
学习过操作系统这门课后,对进程和线程两个概念应该都比较熟悉。进程的出现是为了更充分的利用CPU的资源使得能够并发执行任务。线程则是为了解决并发执行任务过程中,切换进程的开销过大的问题。但线程仍存在问题:(1)线程的切换仍然需要经由用户态->内核态->用户态的转换;(2)内核的调度对象是进程,用户线程发生IO阻塞时,整个进程都会被阻塞。
协程可以简单理解为轻量级线程,用户自己实现对协程的切换调用,不用经过内核态的转换,因此速度更快。Go语言天然支持协程,使用go关键字创建协程。
go func(params ...types){
...
}(args)
管道Channel
Go语言中有两种方式实现通信
- 通过通信共享内存(Channel)
- 通过共享内存实现通信(Lock)
用共享内存的方式,会对临界区加锁,可能发生数据静态的问题,影响性能,所以推荐采用Channel的方式。
Go语言使用make(chan type, [size])创建通道
- 无缓冲通道
make(chan int):通道两端必须同时准备好发送和接受,才能实现通信。 - 有缓存通道
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
}
}
WaitGroup
WaitGroup是为了解决并发任务的等待问题而出现的,假设有几个并行的Goroutine,在他们都完成后,我们将继续进行后续的操作,手动轮询和等待的方式明显是不切合实际的,WaitGroup是一个很好的工具,主体上提供3个函数,供我们完成,在命令行词典中,也做了基本的实践。
#依赖管理
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 离线包安装方法配置。
Apifox模拟结果如下图所示
数据库的版本有待后续实现。