01 介绍
这个项目是一个简单的社区话题系统。 原仓库地址- [Moonlight-Zhao]
02 需求
需求模型来源:青训营话题页
目前该demo已经实现的功能有:
1.展示话题(标题,文字描述)和回帖列表的后端http接口;
2.支持对话题发布回帖;
3.使用mysql保存用户、话题与回帖列表数据。
本文为该demo添加发布话题功能。
03 发现的一个bug
在新功能的实现前,先说说我发现的一个bug。
发现过程
在实现了发布话题这个功能后,使用Get请求查询新发布的话题页面信息,但服务器返回了一个错误:
分析过程
在发布新的topic后,直接查看mysql中的记录,可以看到该topic已经被成功加入到topic表中,如下图所示:
根据服务器返回的错误信息“has no topic user info”,定位到
query_page_info.go的packPageInfo()函数中,这里是要获取发布该话题的用户id:
func (f *QueryPageInfoFlow) packPageInfo() error {
//topic info
userMap := f.userMap
topicUser, ok := userMap[f.topic.UserId]
if !ok {
return errors.New("has no topic user info")
}
// ...
}
可以看到该错误是由于userMap中没有f.topic.UserId这个键值引起的。再找到设置userMap内容的地方:
uids := []int64{f.topic.Id}
for _, post := range f.posts {
uids = append(uids, post.Id)
}
userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
if err != nil {
return err
}
f.userMap = userMap
发现uids这个切片插入了f.topic.Id,这里应该是f.topic.UserId才对。
解决
100| - uids := []int64{f.topic.Id}
100| + uids := []int64{f.topic.UserId}
后续
其实这个bug在原仓库中已经有人发布了Pr,不过仓库作者一直没有处理,可是这个Pr除了修复了这个bug之外还增加了许多自己的注释,也没有说清楚自己解决了什么问题。
当然也有可能是仓库作者很久来看过了。
04 发布话题功能实现
发布话题的功能完全可以仿照发布回帖的功能来完成实现。
首先查看repository/topic.go中的Topic结构体,看看一个话题包含了哪些内容:
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"`
}
其中需要发布者指定的信息是UserId,Title,Content。回到server.go中声明发布话题的接口"community/topic/do",
在handler包中添加PublishTopic()函数,在service包中添加PublishTopic()函数、NewPublishTopicFlow()函数、PublishTopicFlow结构体以及其所需要的方法,最后在repository包中为TopicDao结构体添加CreateTopic()方法。
具体的实现与发布回帖对应的函数实现基本一致。
测试
使用Postman向服务器发送相应的Post请求,可以看到服务器返回的PageInfo,topic_id为2,如下图所示:
再发送Get请求来获取topic2的PageInfo(修复上文提到的bug后),可以看见服务器返回了该话题的信息,如下图所示:
05 细节
架构
本项目使用的是MVC(Model-View-Controller)架构模式:
handler 包(controller)负责控制请求处理
service 包负责处理业务逻辑
如定义了与帖子发布和页面信息查询相关的服务函数
repository 包负责与数据库交互。
main包主要负责启动服务和定义路由。
单例模式
本项目中postDao,topicDao,userDao使用了单例模式。
优势
单例模式保证了postDao,topicDao,userDao仅有一个实例,避免了多次创建实例和资源浪费,并提供一个访问它的全局访问点(在本项目中,userDao的访问点就是下文代码中part 2所返回的userDao实例)。
实现
在golang中,单例模式可以使用sync.Once实现,以UserDao为例:
var userDao *UserDao
var userOnce sync.Once
func NewUserDaoInstance() *UserDao {
// part 1
userOnce.Do(
func() {
userDao = &UserDao{}
})
// part 2
return userDao
}
sync.Once是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似:
- init 函数是在文件包首次被加载的时候执行,且只执行一次
- sync.Once 是在代码运行中需要的时候执行,且只执行一次
NewUserDaoInstance()方法在query_page_info.go中被调用:
userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
NewUserDaoInstance()中part 1部分代码(part1、2定义见上文)只有在第一次运行时才会执行,后续执行时只会执行part 2部分代码。由此在程序运行过程中就不会重复创建UserDao实例,而是一直沿用第一次执行时创建的实例,从而避免了资源浪费。
命名
在本项目里两个关键字的含义。
Dao
如PostDao、NewPostDaoInstance()等。 Dao是指数据访问对象(data access object),是一种设计模式,用于将数据访问逻辑与业务逻辑分离以提供对数据存储的访问和操作。
Flow
如QueryPageInfoFlow、NewQueryPageInfoFlow()等。Flow的含义是过程、流程。例如在名为"NewQueryPageInfoFlow"的函数中,"Flow"可能表示该函数用于管理和控制查询页面信息的整个流程。它可能涉及多个步骤、操作或数据流的流动,以完成特定的任务。