这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。主要内容为对第一次与第二次课程的学习总结。
go语言基础
相关内容已总结在上一篇笔记中juejin.cn/post/709480…
go并发
- Goroutine
在Go语言中,每一个并发的执行单元叫作一个goroutine。和操作系统的线程调度区别在于,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。
Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数。
- Channel
Channel类似一个管道,方便并发核心单元通讯。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
创建一个channel:
ch := make(chan int) // ch has type 'chan int' 无缓冲
ch := make(chan int,2) // 有缓冲
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。
带缓存的Channel内部持有一个元素队列。向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。
关闭一个channel:
close(ch)
- Lock 声明:
lock sync.Mutex
lock.Lock()
/* code */
lock.Unlock()
- WaitGroup
Add(delta int)计数器+delta
Done()计数器-1
Wait()阻塞直到计数器为0。
依赖管理
- Go Module
go.mod文件管理依赖包版本
Proxy 管理依赖库
go get/mod指令管理依赖包
测试
- 单元测试
- 测试文件以
_test.go结尾 - 测试函数
func TestXxx(t *testing.T) - 初始化逻辑放到
TestMain
覆盖率 go test xxx_test.go xxx.go --cover
项目实践
该项目用到了一个web框架gin,项目结构采用了Repository、Service、Controller三层架构。
repository:db_init.go初始化数据库、post.go实现新回帖时保存数据。
service:具体实现请求页面和发布的功能
cotroller:query_page_info.go用来请求获得页面信息,publish_post.go用来给帖子回帖。
课后作业
-
支持发布帖子
-
本地 ID 生成需要保证不重复、唯一性
-
Append 文件,更新索引,注意 Map 的并发安全问题
实现如下:
(1)修改server.go,在main函数中支持发布帖子:
r.POST("/community/topic/do", func(c *gin.Context) {
title, _ := c.GetPostForm("title")
content, _ := c.GetPostForm("content")
data := cotroller.PublishTopic(title, content)
c.JSON(200, data)
})
(2)在cotroller层新增一个文件publish_topic.go
package cotroller
import (
"github.com/Moonlight-Zhao/go-project-example/service"
)
func PublishTopic(title, content string) *PageData {
topicId, err := service.PublishTopic(title, content)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
return &PageData{
Code: 0,
Msg: "success",
Data: map[string]int64{
"topic_id": topicId,
},
}
}
(3)在service层新增一个文件publish_topic.go
package service
import (
"errors"
"time"
"unicode/utf16"
"github.com/Moonlight-Zhao/go-project-example/repository"
)
func PublishTopic(title, content string) (int64, error) {
return NewPublishTopicFlow(title, content).Do()
}
func NewPublishTopicFlow(title, content string) *PublishTopicFlow {
return &PublishTopicFlow{
title: title,
content: content,
}
}
type PublishTopicFlow struct {
title string
content string
topicId int64
}
func (f *PublishTopicFlow) Do() (int64, error) {
if err := f.checkParam(); err != nil {
return 0, err
}
if err := f.publish(); err != nil {
return 0, err
}
return f.topicId, nil
}
func (f *PublishTopicFlow) checkParam() error {
if len(utf16.Encode([]rune(f.content))) >= 500 {
return errors.New("content length must be less than 500")
}
return nil
}
func (f *PublishTopicFlow) publish() error {
topic := &repository.Topic{
Title: f.title,
Content: f.content,
CreateTime: time.Now().Unix(),
}
id, err := idGen.NextId()
if err != nil {
return err
}
topic.Id = id
if err := repository.NewTopicDaoInstance().InsertTopic(topic); err != nil {
return err
}
f.topicId = topic.Id
return nil
}
(4)repository在topic.go中新增函数InsertTopic:
func (*TopicDao) InsertTopic(topic *Topic) error {
f, err := os.OpenFile("./data/topic", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer f.Close()
marshal, _ := json.Marshal(topic)
if _, err = f.WriteString(string(marshal)+"\n"); err != nil {
return err
}
rwMutex.Lock()
_, ok := topicIndexMap[topic.Id]
if !ok {
topicIndexMap[topic.Id] = topic
}
rwMutex.Unlock()
return nil
}