课后作业要求
- 支持发布帖子
- 本地Id生成需要保证不重复、唯一性
- append文件,更新索引,注意Map的并发安全问题
Router
仍然是先设计api,考虑需要的参数,以及请求、传参的方式
-
URL:
/community/post/do -
请求方式:post
-
传参方式:form-data
- topic_id (针对什么话题的回复)
- content (回复内容)
r.POST("/community/post/do", func(c *gin.Context) {
topicId, _ := c.GetPostForm("topic_id")
content, _ := c.GetPostForm("content")
data := cotroller.PublishPost(topicId, content)
c.JSON(200, data)
})
controller层
这一层没有什么本质的变化,仍然负责处理用户的输入和输出,不包含复杂的业务逻辑。
输入的数据在我们设计接口的时候就已经决定下来了,一个topic_id,为回帖内容在哪个话题之下,一个content,为具体的回帖内容
id仍然使用标准strconv库,将string类型转化为int64类型,为后续service层做准备,当然错误处理也不要忘记
func PublishPost(topicIdStr, content string) *PageData {
//参数转换
topicId, _ := strconv.ParseInt(topicIdStr, 10, 64)
//获取service层结果
postId, err := service.PublishPost(topicId, content)
if err != nil {...}
return &PageData{
Code: 0,
Msg: "success",
Data: map[string]int64{
"post_id": postId,
},
}
}
回来的数据依然为标准的PageData结构体
-
PageDate
- code(
int64 -1为false 0为true) - msg(
string 具体的错误信息) - data(
inerface{} 一个匿名的空接口,表示任意类型)
- code(
这里Data 字段的值为一个 map,该 map 包含一个键为 "post_id",值为 postId 的映射。postId 是一个 int64 类型的变量(这跟我们上面的转换一一对应),发布者提交content和topic_id后,post_id由服务器内部生成,返回。
service层
仍然将业务划分为小任务流程,相比例子中的检验传参,获取所需信息,打包三步,这里稍微加快一点,只有检验参数,发布回帖两步(其实就是后面二合一了)
仍然绑定为结构体方法,便于后续函数内部的设计,这里设计的结构体为 PublishPostFlow
-
PublishPostFlow
- topicId (
int64) - content (
string) - postId(
int64)
- topicId (
检验传参
这里主要考虑到回帖的长度问题,设置为不大于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的内容,参照post文件中存储的格式,我们还需要为这条消息生成一个不重复的id和create_time
create_time
生成时间简单,一行代码解决time.Now().Unix
id
不重复的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了
post := &repository.Post{
ParentId: f.topicId,
Content: f.content,
CreateTime: time.Now().Unix(),
Id: id
}
我们需要将这个数据给写回去,更新索引,但这里有个问题,便是并发问题,如果多条数据一起更新,那文件可能会烂掉,因此需要考虑下在某个程序更新这个文件时,别的文件不同动,得加锁。
方法思路
- 打开
post文件 - 将给定的
post结构体转换为 JSON 格式的字节切片,并将post的 JSON 格式数据写入文件。 - 使用读写互斥锁,用于保护
postIndexMap的并发访问。 - 找到与
ParentId相关联的帖子列表,如果没有,则创建一个新列表。 - 更新
postIndexMap中的帖子列表。 - 解锁读写互斥锁,释放资源。
func (*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生成,map并发问题如何加锁等。