课后作业|青训营

144 阅读3分钟

课后作业要求

  • 支持发布帖子
  • 保证本地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{} 一个匿名的空接口,表示任意类型)

服务层(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生成和并发安全问题等。