[ Golang社区项目实战 | 青训营笔记]

258 阅读4分钟

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

需求背景

本文介绍模拟掘金社区服务端的实现。社区话题页面的功能主要包括话题详情,回帖列表,点赞和回帖回复

页面示意图如下:

image-20230226104442133.png

image-20230226104856348.png

功能点总结:

  • 展示话题(标题,文字描述)
  • 回帖列表

项目框架设计

  1. 组件工具

    • Gin高性能go web框架

      Gin是一个使用Go语言开发的Web框架。它提供类似Martini的API,但性能更佳,速度提升高达40倍。

    • Go Mod

  2. 数据库设计

    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');
    

开发实践

  1. 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
      )
      
  2. 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
      }
      
  3. 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,
      	}
      }
      
  4. 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()
    
  5. 分层分析

    • 整体分为三层,repository数据层, service逻辑层 ,controoler视图层;
    • 数据层关联底层数据模型,也就是这里的model, 封装外部数据的增删改查,我们的数据存储在本地文件, 通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明, 屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的;
    • Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy, 对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;
    • Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。

参考资料

‍‌⁢⁢⁣⁤‬‍‌⁢⁡⁤‬⁡‬⁢‌⁣⁡⁢‌⁤‬‍‍⁣⁡‍⁢⁢‌⁤⁤Go 语言入门 - 工程实践 .pptx - 飞书云文档 (feishu.cn)

github.com/Moonlight-Z…