这是我参与「第三届青训营-后端场」笔记创作活动的第2篇笔记。
青训营话题页
需求
- 实现一个展示话题(标题,文字描述)和回帖列表的后端 http 接口;
- 本地文件存储数据 组件及技术点
-
- 了解go web框架的简单使用
-
- 了解分层设计的概念
-
文件操作:读文件pkg.go.dev/io
课堂例子
因为这个例子中并没有采用数据库,因此采用文件来存储数据,每一条数据都是一个json字符串,如下图
通过定义Post、Topic结构体来作为实体化对象
type Post struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
复制代码
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
复制代码
因为数据是存储在文件里,如果每次操作都打开文件会影响程序的运行时间,增加开销,因此我们在程序初始化时就应该把数据读取出来并且建立索引,这样之后就可以直接对索引操作,速度会明显提升:
通过创建两个map来作为索引,map的时间复杂度为O(1)
- TopicIndexMap是根据话题id号来作为key,value为Topic实体
- postIndexMap是根据所属话题id号来作为key,value为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
if err := json.Unmarshal([]byte(text), &topic); 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
}
scanner := bufio.NewScanner(open)
postTmpMap := make(map[int64][]*Post)
for scanner.Scan() {
text := scanner.Text()
var post Post
//将扫描到的帖子数据序列化为一个json对象
if err := json.Unmarshal([]byte(text), &post); err != nil {
return err
}
posts, ok := postTmpMap[post.ParentId]
//如果话题id不存在于postTmpMap中
if !ok {
//空索引实现
postTmpMap[post.ParentId] = []*Post{&post}
continue
}
//如果话题id存在,则直接将每个post追加到post切片中
posts = append(posts, &post)
//建立索引
postTmpMap[post.ParentId] = posts
}
postIndexMap = postTmpMap
return nil
}
复制代码
再从main函数出发:
gin框架路由初始化,添加一个GET路由请求处理,解析请求参数,通过请求参数的topicId来获取话题信息和话题所有的帖子信息
在控制层的cotroller中,定义了QueryPageInfo函数用于查询页面数据
func QueryPageInfo(topicIdStr string) *PageData {
//转化为整型
topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
//在服务层中获取数据
pageInfo, err := service.QueryPageInfo(topicId)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}
复制代码
通过服务层来获取数据,又因为获取topic信息和post列表是互不干扰的,因此可开启两个协程来加速获取
获取数据后便可以作为响应返回:
开启gin,监听3000端口:
运行,postman发起请求,请求成功,成功获取数据
课后作业
-
支持对话题发布回帖。
-
回帖 id 生成需要保证不重复、唯一性。
-
新加回帖追加到本地文件,同时需要更新索引,注意 Map 的并发安全问题。
思路:前端发起Post请求,将帖子信息(帖子内容,话题id)通过表单发送给后端,后端解析表单参数,再根据当前时间戳来生成唯一帖子id(在非高并发的情况下,id是唯一的,也可以通过雪花算法来生成唯一id),然后对参数处理,生成对应的Post实体,写入文件中,并更新索引。
主要代码:
这里是添加一个POST请求的路由处理,解析表单参数,传给控制层 ReleasePost函数对数据进行处理,并且生成当前时间戳作为帖子发布时间
服务层:
func ReleasePost(parent_id int64, content string, create_time int64) error {
return NewReleasePostFlow(parent_id, content, create_time).Do()
}
func NewReleasePostFlow(parent_id int64, content string, create_time int64) *ReleasePostFlow {
return &ReleasePostFlow{
ParentId: parent_id,
Content: content,
CreateTime: create_time,
}
}
type ReleasePostFlow struct {
ParentId int64
Content string
CreateTime int64
}
func (f *ReleasePostFlow) Do() error {
if err := f.checkParam(); err != nil {
return err
}
if err := f.addPost(); err != nil {
return err
}
return nil
}
//检验参数
func (f *ReleasePostFlow) checkParam() error {
//检验帖子内容是否为空
if f.Content == "" {
return errors.New("content is not empty! ")
}
//检验话题id是否存在
_, ok := repository.TopicIndexMap[f.ParentId]
if !ok {
return errors.New("topic id not exist")
}
return nil
}
func (f *ReleasePostFlow) addPost() error {
//使用当前时间戳来生成唯一ID,也可以使用雪花算法生成帖子id
id := f.CreateTime + 10
//生成Post实例化
Post := repository.Post{
Id: id,
ParentId: f.ParentId,
Content: f.Content,
CreateTime: f.CreateTime,
}
err := repository.AddPost(Post)
if err != nil {
return err
}
return nil
}
复制代码
在数据层中对文件进行操作,写入成功后更新索引
运行,postman模拟发起请求:
请求成功!文件中也成功写入!
再通过GET请求获取,也成功获取新添加的帖子,说明索引更新成功!