课上简易社区及话题管理项目流程跑通 | 青训营

159 阅读10分钟

简易社区及话题管理

本文代码和相关图片来自字节青训营赵征老师的Go语言工程实践课程,我主要看了下项目源码,了解开发一个简易社区及话题管理项目的流程及项目框架。在此记录,如有理解错误,望指正。

image-20230727195637338.png

1. 新建项目

在GoLand中创建新项目SimpleCommunity

image-20230727170209918.png

创建完项目应该会自动生成一个go.mod文件,如果没有,可以执行go mod init来生成。

2. 安装高性能go web框架——Gin

在终端执行go get gopkg.in/gin-gonic/gin.v1@v1.3.0来安装Gin,有时会报下面的错误:

image-20230727192041014.png

怀疑是版本问题,于是采用go get -u github.com/gin-gonic/gin安装了最新版的Gin:

image-20230727191932114.png

:import外部包可以通过go get xxx来下载

3. 编写与数据库交互相关的结构体及函数

  1. 在项目文件夹SimpleCommunity下新建仓库文件夹repository

  2. repository文件夹下新建相关go文件。

    • topic.go:创建社区话题的结构体,定义相关方法及函数。

      package repository
      
      import (
      	"SimpleCommunity/util"
      	"sync"
      	"time"
      )
      
      // 话题的结构体
      type Topic struct {
      	Id         int64     `gorm:"column:id"`
      	UserId     int64     `gorm:"column:user_id"`
      	Title      string    `gorm:"column:title"`
      	Content    string    `gorm:"column:content"`
      	CreateTime time.Time `gorm:"column:create_time"`
      }
      
      func (Topic) TableName() string {
      	return "topic"
      }
      
      type TopicDao struct {
      }
      
      var topicDao *TopicDao
      var topicOnce sync.Once // 高并发的情况下保证只执行一次
      
      // 创建一个topic实例
      func NewTopicDaoInstance() *TopicDao {
      	topicOnce.Do(
      		func() {
      			topicDao = &TopicDao{}
      		})
      	return topicDao
      }
      
      // 根据id查询话题
      func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {
      	var topic Topic
      	err := db.Where("id = ?", id).Find(&topic).Error
      	if err != nil {
      		util.Logger.Error("find topic by id err:" + err.Error())
      		return nil, err
      	}
      	return &topic, nil // 未报错则返回查询到的topic
      }
      
      

      QueryTopicById实现根据id来查询topic

      sync.Once适合在高并发的情况下只执行一次的应用场景

      这样,在数据层对文件(数据库)中topic的一些查询操作就完成了,返回的是定义好的结构体。

    • post.go:创建帖子的结构体,定义相关方法及函数,如根据id来查询topic(QueryTopicById

      package repository
      
      import (
      	"SimpleCommunity/util"
      	"gorm.io/gorm"
      	"sync"
      	"time"
      )
      
      // 帖子结构体
      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
      
      // 创建一个post实例
      func NewPostDaoInstance() *PostDao {
      	postOnce.Do(
      		func() {
      			postDao = &PostDao{}
      		})
      	return postDao
      }
      
      // 根据id查询帖子
      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
      }
      
      // 根据parent_id查询帖子
      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
      }
      
      

      QueryPostById实现根据id来查询post

      QueryPostByParentId实现根据parent_id来查询post

      CreatePost实现在数据库中插入一条新帖子。

      这样,在数据层对文件(数据库)中post的一些查询操作就完成了,返回的是定义好的结构体。

    • db_init.go:初始化数据库

      这里有gorm中文文档

      package repository
      
      import (
      	"gorm.io/driver/mysql"
      	"gorm.io/gorm"
      )
      
      var db *gorm.DB
      
      func Init() error {
      	var err error
      	dsn := "root:00000000@tcp(127.0.0.1:3306)/community?charset=utf8mb4&parseTime=True&loc=Local"
      	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) // 连接数据库
      	return err
      }
      
      
    • user.go:创建社区用户的结构体,包含社区用户的一些个人信息以及发帖信息,定义相关方法及函数。

      package repository
      
      import (
         "SimpleCommunity/util"
         "gorm.io/gorm"
         "sync"
         "time"
      )
      
      type User struct {
         Id         int64     `gorm:"column:id"`
         Name       string    `gorm:"column:name"`
         Avatar     string    `gorm:"column:avatar"`
         Level      int       `gorm:"column:level"`
         CreateTime time.Time `gorm:"column:create_time"`
         ModifyTime time.Time `gorm:"column:modify_time"`
      }
      
      func (User) TableName() string {
         return "user"
      }
      
      type UserDao struct {
      }
      
      var userDao *UserDao
      var userOnce sync.Once
      
      // 创建一个user实例
      func NewUserDaoInstance() *UserDao {
         userOnce.Do(
            func() {
               userDao = &UserDao{}
            })
         return userDao
      }
      
      // 根据id查询用户
      func (*UserDao) QueryUserById(id int64) (*User, error) {
         var user User
         err := db.Where("id = ?", id).Find(&user).Error
         if err == gorm.ErrRecordNotFound {
            return nil, nil
         }
         if err != nil {
            util.Logger.Error("find user by id err:" + err.Error())
            return nil, err
         }
         return &user, nil
      }
      
      // 根据id批量查询用户
      func (*UserDao) MQueryUserById(ids []int64) (map[int64]*User, error) {
         var users []*User
         err := db.Where("id in (?)", ids).Find(&users).Error
         if err != nil {
            util.Logger.Error("batch find user by id err:" + err.Error())
            return nil, err
         }
         userMap := make(map[int64]*User)
         for _, user := range users {
            userMap[user.Id] = user
         }
         return userMap, nil
      }
      

      QueryUserById实现根据id查询用户

      MQueryUserById实现根据id批量查询用户。

4. 编写逻辑层处理核心业务

如果说数据层是程序与数据库进行交互,然后将操作数据库(查询、插入等)的相关方法暴露出来的话,那么逻辑层就需要处理社区话题这一核心业务的逻辑处理及输出了。

  1. 在项目文件夹SimpleCommunity下新建业务逻辑文件夹service

  2. service文件夹下新建相关go文件。

    • query_page_info.go:声明的结构体有页面信息、topic以及post相关信息,它们的关系如下图:

      Central Topic.png

      • 定义查询页面信息的函数QueryPageInfo,查询页面信息主要通过一个结构体QueryPageInfoFlow,一个页面由一个话题及下面的多个回帖组成,因此需要传入话题的topicId

      • 在查询前需要检查参数是否合法,排除非法值(checkParam())。

      • 然后从数据库中执行查询操作并将结果封装到QueryPageInfoFlow结构体中,来准备要返回的页面信息(prepareInfo())。期间可以使用WaitGroup进行多个任务的同步WaitGroup可以保证在并发环境中完成指定数量的任务。这里要等待QueryTopicById(f.topicId)QueryPostByParentId(f.topicId)两个goroutine的完成,调用的就是之前在repository中写的有关数据库访问操作的方法。但根据源码写的,该方法最后还要根据查询获取topic和post的用户信息并存到userMap,传入的uids却是topic和post的id,我认为通过id查找user应该将topic和post的UserId传入uids

      • 还有一个方法就是封装页面信息packPageInfo()),根据userMap获取topicUserpostUser,然后加上QueryPageInfoFlow保存的topicposts就可以构建pageInfo

      package service
      
      import (
      	"SimpleCommunity/repository"
      	"errors"
      	"fmt"
      	"sync"
      )
      
      // topic相关信息,包括话题和用户
      type TopicInfo struct {
      	Topic *repository.Topic
      	User  *repository.User
      }
      
      // post相关信息,包括帖子和用户
      type PostInfo struct {
      	Post *repository.Post
      	User *repository.User
      }
      
      // 页面信息,包括话题信息和帖子列表
      type PageInfo struct {
      	TopicInfo *TopicInfo
      	PostList  []*PostInfo
      }
      
      func QueryPageInfo(topicId int64) (*PageInfo, error) {
      	return NewQueryPageInfoFlow(topicId).Do()
      }
      
      // 创建查询页面信息的flow
      func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
      	return &QueryPageInfoFlow{
      		topicId: topId,
      	}
      }
      
      // 查询页面信息的flow结构体
      type QueryPageInfoFlow struct {
      	topicId  int64
      	pageInfo *PageInfo
      
      	topic   *repository.Topic
      	posts   []*repository.Post
      	userMap map[int64]*repository.User
      }
      
      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
      }
      
      // 检查参数是否合法
      func (f *QueryPageInfoFlow) checkParam() error {
      	if f.topicId <= 0 {
      		return errors.New("topic id must be larger than 0")
      	}
      	return nil
      }
      
      // 准备页面信息并封装到flow结构体中
      func (f *QueryPageInfoFlow) prepareInfo() error {
      	//获取topic信息
      	var wg sync.WaitGroup
      	wg.Add(2)
      	var topicErr, postErr error
      	go func() {
      		defer wg.Done()
      		topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
      		if err != nil {
      			topicErr = err
      			return
      		}
      		f.topic = topic
      	}()
      	//获取post列表
      	go func() {
      		defer wg.Done()
      		posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId)
      		if err != nil {
      			postErr = err
      			return
      		}
      		f.posts = posts
      	}()
      	wg.Wait()
      	if topicErr != nil {
      		return topicErr
      	}
      	if postErr != nil {
      		return postErr
      	}
      	//获取用户信息
      	uids := []int64{f.topic.UserId}
      	// uids := []int64{f.topic.Id} // 我认为这里应该传入topic.UserId
      	for _, post := range f.posts {
      		uids = append(uids, post.UserId)
      		//uids = append(uids, post.Id) // 我认为这里应该传入post.UserId
      	}
      	userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
      	if err != nil {
      		return err
      	}
      	f.userMap = userMap
      	return nil
      }
      
      // 封装页面信息
      func (f *QueryPageInfoFlow) packPageInfo() error {
      	//topic info
      	userMap := f.userMap //	userMap是userId到user的映射
      	topicUser, ok := userMap[f.topic.UserId]
      	if !ok {
      		return errors.New("has no topic user info")
      	}
      	//post list
      	postList := make([]*PostInfo, 0)
      	for _, post := range f.posts {
      		postUser, ok := userMap[post.UserId]
      		if !ok {
      			return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
      		}
      		postList = append(postList, &PostInfo{
      			Post: post,
      			User: postUser,
      		})
      	}
      	f.pageInfo = &PageInfo{
      		TopicInfo: &TopicInfo{
      			Topic: f.topic,
      			User:  topicUser,
      		},
      		PostList: postList,
      	}
      	return nil
      }
      
    • publish_post.go:实现发帖的功能。

      • 定义发布帖子的flow结构体,主要包含用户userId、话题topicId、帖子postId以及帖子内容content

      • 同样要对非法参数进行检查(checkParam()),避免非法userId、过长的内容等。

      • 最后就是发帖的函数(publish()),主要就是构建一个post结构体然后调用repository的CreatePost()方法在数据库中插入一条新帖数据。

      package service
      
      import (
      	"SimpleCommunity/repository"
      	"errors"
      	"time"
      	"unicode/utf8"
      )
      
      // 发布帖子
      func PublishPost(topicId, userId int64, content string) (int64, error) {
      	return NewPublishPostFlow(topicId, userId, content).Do()
      }
      
      func NewPublishPostFlow(topicId, userId int64, content string) *PublishPostFlow {
      	return &PublishPostFlow{
      		userId:  userId,
      		content: content,
      		topicId: topicId,
      	}
      }
      
      // 发布帖子的flow结构体
      type PublishPostFlow struct {
      	userId  int64
      	content string
      	topicId int64
      	postId  int64
      }
      
      func (f *PublishPostFlow) Do() (int64, error) {
      	if err := f.checkParam(); err != nil {
      		return 0, err
      	}
      	if err := f.publish(); err != nil {
      		return 0, err
      	}
      	return f.postId, nil
      }
      
      func (f *PublishPostFlow) checkParam() error {
      	if f.userId <= 0 {
      		return errors.New("userId id must be larger than 0")
      	}
      	if utf8.RuneCountInString(f.content) >= 500 {
      		return errors.New("content length must be less than 500")
      	}
      	return nil
      }
      
      func (f *PublishPostFlow) publish() error {
      	post := &repository.Post{
      		ParentId:   f.topicId,
      		UserId:     f.userId,
      		Content:    f.content,
      		CreateTime: time.Now(),
      	}
      	if err := repository.NewPostDaoInstance().CreatePost(post); err != nil {
      		return err
      	}
      	f.postId = post.Id
      	return nil
      }
      
  3. service文件夹下新建对应单元测试文件

    • query_page_info_test.go:测试查询页面的功能。
    • publish_post_test.go:测试对话题发布回帖的功能。

5. 编写client访问社区功能的接口

service层将业务逻辑实现后,就需要编写接口接收client的请求数据,然后调用service层实现的对应函数,并将获取到的结果返回给client。

  1. 在项目文件夹SimpleCommunity下新建client handler文件夹handler

  2. handler文件夹下新建相关go文件。

    • query_page_info.go

      • 定义页面数据结构体PageData,与json数据对应
      • 编写查询页面信息的函数QueryPageInfo()。由于传入的topicId是string类型,因此先要做个类型转换成int64。然后调用service.QueryPageInfo(topicId)获取service层的结果。最后将PageData返回。
      package handler
      
      import (
      	"SimpleCommunity/service"
      	"strconv"
      )
      
      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,
      	}
      
      }
      
    • publish_post.go

      • 编写发布帖子的函数PublishPost()。调用service.PublishPost(topic, uid, content)来将发布的帖子写入数据库,并返回PageData
      package handler
      
      import (
      	"SimpleCommunity/service"
      	"strconv"
      )
      
      // 发布帖子
      func PublishPost(uidStr, topicIdStr, content string) *PageData {
      	//参数转换
      	uid, _ := strconv.ParseInt(uidStr, 10, 64)
      	topic, _ := strconv.ParseInt(topicIdStr, 10, 64)
      	//获取service层结果
      	postId, err := service.PublishPost(topic, uid, content)
      	if err != nil {
      		return &PageData{
      			Code: -1,
      			Msg:  err.Error(),
      		}
      	}
      	return &PageData{
      		Code: 0,
      		Msg:  "success",
      		Data: map[string]int64{
      			"post_id": postId,
      		},
      	}
      
      }
      

6. 添加工具包

  1. 在项目文件夹SimpleCommunity下新建工具文件夹util

  2. util文件夹下新建logger.go来引入日志。

    package util
    
    import "go.uber.org/zap"
    
    var Logger *zap.Logger
    
    func InitLogger() error {
    	var err error
    	Logger, err = zap.NewProduction()
    	if err != nil {
    		return err
    	}
    	return nil
    }
    

7. 编写sql文件作为数据库

在项目文件夹SimpleCommunity下新建sql文件example.sql

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

8. 完成server初始化并模拟client对server的请求

在项目文件夹SimpleCommunity下新建server.go。编写main函数完成repository和logger的初始化并模拟GET和POST请求。这里就是整个项目的入口

package main

import (
	"SimpleCommunity/handler"
	"SimpleCommunity/repository"
	"SimpleCommunity/util"
	"github.com/gin-gonic/gin"
	"os"
)

func main() {
	if err := Init(); err != nil {
		os.Exit(-1)
	}
	r := gin.Default()

	r.Use(gin.Logger())

	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()
	if err != nil {
		return
	}
}

func Init() error {
	if err := repository.Init(); err != nil {
		return err
	}
	if err := util.InitLogger(); err != nil {
		return err
	}
	return nil
}

9. 运行体验建议社区功能及debug

  1. 运行server.go的main函数

  2. 报错数据库初始化失败:

    image-20230728112930271.png

    原来是我没安装MySQL ==!

    解决:

    • 下载安装mysql

    • 下载安装Navicat

    • 打开Navicat,新建localhost_3306连接,然后新建community数据库,在community数据库中运行SQL文件(项目文件中的example.sql),就可以看到利用sql文件生成的数据库了。

      image-20230728201446557.png

    • 修改db_init.go里的dsn为自己的数据库连接信息。dsn格式如下:

      //mysql dsn格式
      //涉及参数:
      //username   数据库账号
      //password   数据库密码
      //host       数据库连接地址,可以是Ip或者域名
      //port       数据库端口
      //Dbname     数据库名
      username:password@tcp(host:port)/Dbname?charset=utf8&parseTime=True&loc=Local
      
    • 之后运行server.go就可以成功连上数据库了,开始监听并处理8080端口的HTTP请求。

      image-20230728203107107.png

  3. 打开浏览器,访问127.0.0.1:8080/ping就可以看到访问到了在server.go中设置的json数据。确实能ping通,并且后台也能看到请求信息。

    image-20230728203024935.png