简易社区及话题管理
本文代码和相关图片来自字节青训营赵征老师的Go语言工程实践课程,我主要看了下项目源码,了解开发一个简易社区及话题管理项目的流程及项目框架。在此记录,如有理解错误,望指正。
1. 新建项目
在GoLand中创建新项目SimpleCommunity:
创建完项目应该会自动生成一个go.mod文件,如果没有,可以执行go mod init来生成。
2. 安装高性能go web框架——Gin
在终端执行go get gopkg.in/gin-gonic/gin.v1@v1.3.0来安装Gin,有时会报下面的错误:
怀疑是版本问题,于是采用go get -u github.com/gin-gonic/gin安装了最新版的Gin:
注:import外部包可以通过go get xxx来下载
3. 编写与数据库交互相关的结构体及函数
-
在项目文件夹
SimpleCommunity下新建仓库文件夹repository。 -
在
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. 编写逻辑层处理核心业务
如果说数据层是程序与数据库进行交互,然后将操作数据库(查询、插入等)的相关方法暴露出来的话,那么逻辑层就需要处理社区话题这一核心业务的逻辑处理及输出了。
-
在项目文件夹
SimpleCommunity下新建业务逻辑文件夹service。 -
在
service文件夹下新建相关go文件。-
query_page_info.go:声明的结构体有页面信息、topic以及post相关信息,它们的关系如下图:-
定义查询页面信息的函数
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获取topicUser和postUser,然后加上QueryPageInfoFlow保存的topic和posts就可以构建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 } -
-
-
在
service文件夹下新建对应单元测试文件query_page_info_test.go:测试查询页面的功能。publish_post_test.go:测试对话题发布回帖的功能。
5. 编写client访问社区功能的接口
service层将业务逻辑实现后,就需要编写接口接收client的请求数据,然后调用service层实现的对应函数,并将获取到的结果返回给client。
-
在项目文件夹
SimpleCommunity下新建client handler文件夹handler。 -
在
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. 添加工具包
-
在项目文件夹
SimpleCommunity下新建工具文件夹util。 -
在
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
-
运行
server.go的main函数 -
报错数据库初始化失败:
原来是我没安装MySQL ==!
解决:
-
下载安装mysql
-
下载安装Navicat
-
打开Navicat,新建localhost_3306连接,然后新建community数据库,在community数据库中运行SQL文件(项目文件中的
example.sql),就可以看到利用sql文件生成的数据库了。 -
修改
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请求。
-
-
打开浏览器,访问
127.0.0.1:8080/ping就可以看到访问到了在server.go中设置的json数据。确实能ping通,并且后台也能看到请求信息。