这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天
需求背景
本文介绍模拟掘金社区服务端的实现。社区话题页面的功能主要包括话题详情,回帖列表,点赞和回帖回复
页面示意图如下:
功能点总结:
- 展示话题(标题,文字描述)
- 回帖列表
项目框架设计
-
组件工具
-
Gin高性能go web框架
Gin是一个使用Go语言开发的Web框架。它提供类似Martini的API,但性能更佳,速度提升高达40倍。
-
Go Mod
-
-
数据库设计
CREATE DATABASE IF NOT EXISTS `community` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */; USE `community`; DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` varchar(128) NOT NULL DEFAULT '' COMMENT '用户昵称', `avatar` varchar(128) NOT NULL DEFAULT '' COMMENT '头像', `level` int(10) NOT NULL DEFAULT 1 COMMENT '用户等级', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='用户表'; INSERT INTO `user` VALUES (1, 'Jerry', '', 1, '2022-04-01 10:00:00', '2022-04-01 10:00:00'), (2, 'Tom', '', 2, '2022-04-01 10:00:00', '2022-04-01 10:00:00'); DROP TABLE IF EXISTS `topic`; CREATE TABLE `topic` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id', `user_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '用户id', `title` varchar(128) NOT NULL default '' COMMENT '标题', `content` text NOT NULL COMMENT '头像', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='话题表'; INSERT INTO `topic` VALUES (1, 1, '青训营开课啦', '快到碗里来!', '2022-04-01 13:50:19'); DROP TABLE IF EXISTS `post`; CREATE TABLE `post` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id', `parent_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '父id', `user_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '用户id', `content` text NOT NULL COMMENT '头像', `digg_count` int(10) NOT NULL DEFAULT 0 COMMENT '点赞数', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), INDEX parent_id (`parent_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='回帖表'; INSERT INTO `post` VALUES (1, 1, 1, '举手报名!', 10, '2022-04-01 14:50:19'), (2, 1, 2, '举手报名+1', 20, '2022-04-01 14:51:19');
开发实践
-
Dao层(以Post为例)
type Post struct { Id int64 `gorm:"column:id"` ParentId int64 `gorm:"parent_id"` UserId int64 `gorm:"column:user_id"` Content string `gorm:"column:content"` DiggCount int32 `gorm:"column:digg_count"` CreateTime time.Time `gorm:"column:create_time"` } func (Post) TableName() string { return "post" } type PostDao struct { } var postDao *PostDao var postOnce sync.Once func NewPostDaoInstance() *PostDao { postOnce.Do( func() { postDao = &PostDao{} }) return postDao } func (*PostDao) QueryPostById(id int64) (*Post, error) { var post Post err := db.Where("id = ?", id).Find(&post).Error if err == gorm.ErrRecordNotFound { return nil, nil } if err != nil { util.Logger.Error("find post by id err:" + err.Error()) return nil, err } return &post, nil } func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) { var posts []*Post err := db.Where("parent_id = ?", parentId).Find(&posts).Error if err != nil { util.Logger.Error("find posts by parent_id err:" + err.Error()) return nil, err } return posts, nil } func (*PostDao) CreatePost(post *Post) error { if err := db.Create(post).Error; err != nil { util.Logger.Error("insert post err:" + err.Error()) return err } return nil }这里可以使用内存索引进行简单优化:
-
用Map实现内存索引
-
在服务对外暴露前,利用文件元数据初始化全局内存索引,这样就可以实现0 (1)的时间复杂度查找操作
-
具体实现:
var( topicIndexMap map[int64]*Topic postIndexMap map[int64][]*Post )
-
-
Service层
-
实体描述
type PageInfo struct { TopicInfo *TopicInfo PostList []*PostInfo } -
service流程编排
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) { if err := f.checkParam(); err != nil { return nil, err } if err := f.prepareInfo(); err != nil { return nil, err } if err := f.packPageInfo(); err != nil { return nil, err } return f.pageInfo, nil }
-
-
Controller层
-
构建view对象
type PageData struct { Code int64 `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data"` } -
业务处理返回
func QueryPageInfo(topicIdStr string) *PageData { //参数转换 topicId, err := strconv.ParseInt(topicIdStr, 10, 64) if err != nil { return &PageData{ Code: -1, Msg: err.Error(), } } //获取service层结果 pageInfo, err := service.QueryPageInfo(topicId) if err != nil { return &PageData{ Code: -1, Msg: err.Error(), } } return &PageData{ Code: 0, Msg: "success", Data: pageInfo, } }
-
-
Router
r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.GET("/community/page/get/:id", func(c *gin.Context) { topicId := c.Param("id") data := handler.QueryPageInfo(topicId) c.JSON(200, data) }) r.POST("/community/post/do", func(c *gin.Context) { uid, _ := c.GetPostForm("uid") topicId, _ := c.GetPostForm("topic_id") content, _ := c.GetPostForm("content") data := handler.PublishPost(uid, topicId, content) c.JSON(200, data) }) err := r.Run() -
分层分析
- 整体分为三层,repository数据层, service逻辑层 ,controoler视图层;
- 数据层关联底层数据模型,也就是这里的model, 封装外部数据的增删改查,我们的数据存储在本地文件, 通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明, 屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的;
- Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy, 对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;
- Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。
参考资料
Go 语言入门 - 工程实践 .pptx - 飞书云文档 (feishu.cn)