GO语言工程实践课后作业 | 青训营

68 阅读4分钟

课后作业要求

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

这里Data 字段的值为一个 map,该 map 包含一个键为 "post_id",值为 postId 的映射。postId 是一个 int64 类型的变量(这跟我们上面的转换一一对应),发布者提交contenttopic_id后,post_id由服务器内部生成,返回。

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的内容,参照post文件中存储的格式,我们还需要为这条消息生成一个不重复的idcreate_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并发问题如何加锁等。