课后作业要求
- 支持发布帖子
- 保证本地Id生成不重复且唯一
- 追加文件内容,更新索引,需要注意Map的并发安全问题
Router
首先设计API,考虑所需参数以及请求和传参方式。
- URL:
/community/post/do - 请求方式:POST
- 传参方式:form-data
- topic_id(回复所属话题的ID)
- content(回复内容)
r.POST("/community/post/do", func(c *gin.Context) {
topicId, _ := c.GetPostForm("topic_id")
content, _ := c.GetPostForm("content")
data := controller.PublishPost(topicId, content)
c.JSON(200, data)
})
控制器(Controller)层 这一层没有本质变化,仍负责处理用户输入和输出,不包含复杂业务逻辑。
在接口设计时,已确定输入数据,即 topic_id(回帖所属话题的ID)和 content(回帖内容)。
标准的 strconv 库用于将字符串转换为 int64 类型的ID,为后续服务层做准备,同时要注意错误处理。
func PublishPost(topicIdStr, content string) *PageData {
topicId, _ := strconv.ParseInt(topicIdStr, 10, 64)
postId, err := service.PublishPost(topicId, content)
if err != nil {...}
return &PageData{
Code: 0,
Msg: "success",
Data: map[string]int64{
"post_id": postId,
},
}
}
返回的数据仍然使用标准的 PageData 结构体:
回来的数据依然为标准的PageData结构体
- PageDate
- code(
int64 -1为false 0为true) - msg(
string 具体的错误信息) - data(
inerface{} 一个匿名的空接口,表示任意类型)
- code(
服务层(Service) 仍然将业务分解为小任务流程。相较于示例中的参数验证、获取所需信息和打包等三个步骤,这里稍微加速,仅需参数验证和回帖发布两个步骤(实际上是将后两步合二为一)。
仍然将它们绑定为结构体方法,以方便内部函数设计。这里设计的结构体为 PublishPostFlow。
PublishPostFlow topicId(int64) content(string) postId(int64) 参数验证 此步骤主要考虑回帖内容长度问题,设定为不超过500个字符。
func (f *PublishPostFlow) checkParam() error {
if len(utf16.Encode([]rune(f.content))) >= 500 {
return errors.New("content length must be less than 500")
}
return nil
}
发布回帖 在此之前,我们已经获取了 parent_id 和 content 内容。参考存储格式,在此需要为该消息生成不重复的 id 和 create_time。
create_time 生成时间很简单,一行代码搞定:time.Now().Unix()
id 为保证唯一性,使用现有的 id-worker 包:
import (
idworker "github.com/gitstliu/go-id-worker"
)
var idGen *idworker.IdWorker
func init() {
idGen = &idworker.IdWorker{}
idGen.InitIdWorker(1, 1)
}
id, err := idGen.NextId()
更新索引 在获得所有数据后,我们得到了一个回帖:
post := &repository.Post{
ParentId: f.topicId,
Content: f.content,
CreateTime: time.Now().Unix(),
Id: id,
}
需要将这个数据写回文件并更新索引。但有个问题是并发问题,多条数据同时更新时可能导致文件损坏。因此,在一个程序更新文件时,需锁定其他文件不得修改。
方法如下:
打开 post 文件。 将给定的 post 结构体转换为 JSON 格式的字节切片,并写入文件。 使用读写互斥锁保护 postIndexMap 并发访问。 查找与 ParentId 相关的帖子列表,如果不存在,则创建新列表。 更新 postIndexMap 中的帖子列表。 解锁读写互斥锁,释放资源。
Copy codefunc (*PostDao) InsertPost(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()
marshal, _ := json.Marshal(post)
if _, err = f.WriteString(string(marshal) + "\n"); err != nil {
return err
}
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
}
测试
测试分为初始化和单元测试两步。
初始化 加载数据所在文件夹:
func TestMain(m *testing.M) {
repository.Init("../data/")
os.Exit(m.Run())
}
单元测试 主要测试返回是否为空以及返回数量是否符合更新后的数量:
assert.NotEqual(t, nil, pageInfo)
assert.Equal(t, 8, len(pageInfo.PostList))
结尾
本次课后作业综合运用了许多知识,充分发挥了所学内容的应用空间。在完成过程中可能遇到了多个问题,如ID生成和并发安全问题等。