社区话题页面实践 | 青训营笔记

176 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

社区话题页面实践

Created: January 18, 2023 5:41 PM Tags: golang

开发环境

Go 1.19.5

WSL2 Ubuntu 20.04

VSCode

项目实践

需求背景

该项目的需求是实现一个社区话题页面的服务端,页面的内容包括话题详情,回帖列表,页面支持的操作应当包括回帖,点赞,和回复回帖,作为一个服务端练习的小项目我们只需要实现一个本地 web 服务,将话题详情和回帖列表以 json 文件的方式输出到终端即可,目前支持的页面操作为对话题进行回帖,能够保证回帖 ID 的唯一。整个项目暂不涉及数据库相关功能,仅考虑本地文件存储。俗话说麻雀虽小,五脏俱全,该社区话题页面作为一个服务端入门项目能够帮助我们梳理项目开发的思路和流程,学习不同的功能的处理逻辑,为未来的项目开发打下基础。

项目地址:github.com/Moonlight-Z…

Untitled 1.png

系统设计

了解需求背景之后我们可以用 ER 图来描述需求模型,ER 图也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,是用来描述现实世界的概念模型。如下图所示,存在话题(Topic)和帖子(Post)两个实体类型,其中每个 Topic 都有一个唯一的 topic_id,每个属于该 Topic 的 Post 都将关联该 id 字段,并且每个 Post 也都有一个唯一的 post_id。除此之外 Topic 实体类型还有标题(title)、内容(content)和创建时间(create_time)这些字段,Post 实体类型同样也有 content 和 create_time 字段。

Untitled 2.png

基于实体模型我们将整个服务端分为三层,分别是 repository 数据层,service 逻辑层以及 controller 视图层,其中:

  • 数据层关联底层数据模型,支持数据的增删改查,此次我们的数据是存储在本地文件中,主要是从文件中读取数据并封装到结构体中方便后续使用,然后把数据访问接口暴露给逻辑层。这里附上写的另一篇关于文件操作的帖子
  • 逻辑层主要实现核心业务,在本项目中就是根据话题id来查询相应的话题和帖子列表,并上送给视图层。
  • 视图层负责处理外部的交互逻辑,以 view 视图的形式返回给客户端,在本项目中则是将封装 json 格式化的请求结果通过 api 形式访问。

Untitled 3.png

逻辑实现

1. 主程序 (server.go)

该项目中我们使用到了 gin web 框架,这是一个基于 golang 的微框架,提供了许多常用的 API,源码注释比较明确,具有快速灵活和容错方便等特点。对于 golang 而言,web 框架的依赖要远比 Python,Java 之类的要小。自身的net/http足够简单,性能也非常不错,二借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队统一编码风格和形成规范。下面是一个简单的示例程序:

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    // 1.创建路由
   r := gin.Default()
   // 2.绑定路由规则,执行的函数
   // gin.Context,封装了request和response
   r.GET("/", func(c *gin.Context) {
      c.String(http.StatusOK, "hello World!")
   })
   // 3.监听端口,默认在8080
   // Run("里面不指定端口号默认为8080") 
   r.Run(":8080")
}

Untitled 4.png

可以看到我们在 main() 函数中调用了 gin 相关组件:

func main() {
	if err := Init("./data/"); err != nil {
		os.Exit(-1)
	}
	r := gin.Default()
	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")
		data := cotroller.QueryPageInfo(topicId)
		c.JSON(200, data)
	})
	err := r.Run()
	if err != nil {
		return
	}
}

初始版本程序运行后能够通过外部接口查询某个 topic 下的帖子列表,topic 和 post 数据是在系统启动的时候初始化的,查询某个 topic 的帖子就是直接从 map 中取出对应 topic_id 的 post 列表:

type Topic struct {
	Id         int64  `json:"id"`
	Title      string `json:"title"`
	Content    string `json:"content"`
	CreateTime int64  `json:"create_time"`
}

type Post struct {
	Id         int64  `json:"id"`
	ParentId   int64  `json:"parent_id"`
	Content    string `json:"content"`
	CreateTime int64  `json:"create_time"`
}

var (
	topicIndexMap map[int64]*Topic
	postIndexMap  map[int64][]*Post
)

我们现在需要增加在某个 topic 下发帖的功能,因此在 main() 函数下提供一个外部接口,用于发布帖子,之后还要配套完成 controller、service 和 repository 层的编写:

func main() {
	...
	r.POST("/community/page/post/add", func(c *gin.Context) {
		topic_Id, _ := c.GetPostForm("topic_id")
		content, _ := c.GetPostForm("content")
		data := cotroller.PublishPost(topic_Id, content)
		c.JSON(200, data)
	})
	...
}

2. repository 层

Untitled 5.png

repository 层包含三个文件,分别是 topic.go post.go 和 db_init.go,负责处理底层数据,创建数据模型并向上层暴露数据增删改查接口,原项目提供了主题和帖子的查找功能,现在我们为其添加一个插入帖子的功能,支持我们的发帖操作:

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 {
		// post.parentId 为空
		postIndexMap[post.ParentId] = []*Post{post}
	} else {
		// post.parentId 不为空,插入新 post
		postList = append(postList, post)
		postIndexMap[post.ParentId] = postList
	}
	rwMutex.Unlock()
	return nil
}

考虑到并发发布帖子的情况,因此需要对文件的追加插入和对 map 的追加插入加上互斥锁,为此定义了全局变量 rwMutex

var (
	topicIndexMap map[int64]*Topic
	postIndexMap  map[int64][]*Post
	rwMutex       sync.RWMutex
)

3. service 层

service 层包含 query_page_info.gopublish_post.go 两个文件,query_page_info.go 返回请求页面信息,publish_post.go 则是处理发布帖子的逻辑,其中为了保证生成的 post_id 唯一,使用到了开源项目 go-id-worker 来完成这一功能。

var idGen *idworker.IdWorker

func init() {
	idGen = &idworker.IdWorker{}
	// post_id 从1开始,每次递增1
	idGen.InitIdWorker(1, 1)
}

Untitled 6.png

Untitled 7.png

3. controller 层

最后 controller 视图层需要调用 service 层的查询页面和发布帖子两个功能,并将页面结果返回给客户端,同样包含两个文件 query_page_info.gopublish_post.go

Untitled 8.png

运行

Untitled 9.png

// 查询话题页面
curl --location --request GET 'http://127.0.0.1:8080/community/page/get/1' | jq

// 话题发帖
curl --location --request POST -d '{"topic_id":"1", "content":"newpublish!"}' 'http://127.0.0.1:8080/community/page/post/do' | jq

Untitled 10.png

Untitled 11.png

总结

至此,对该项目代码的学习就结束了,处理逻辑比较清晰,其中使用互斥锁确保了并发安全性,基于分层结构的设计让我们在处理某一层逻辑的时候,只需要关注相邻层的功能接口,一个复杂的问题通过分解成一系统子问题,这样就有效的降低了每个子问题的规模与复杂度。