需求描述
社区话题页面
- 展示话题(标题,文宇描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
该需求涉及到的用例
用户浏览社区页面,该页面包含话题的内容以及回帖列表。
需要用到的实体类
话题(Topic)和回帖(Post)。
// 话题(Topic)结构体
type Topic struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime time.Time `json:"create_time"`
}
// 回帖(Post)结构体
type Post struct {
ID int `json:"id"`
TopicID int `json:"topic_id"`
Content string `json:"content"`
CreateTime time.Time `json:"create_time"`
}
上述结构体中的字段使用了json
标签,这将在后面的json序列化和反序列化中使用。
层级结构
这个项目将按照以下三层架构进行设计和实现:
-
数据层(
Repository
):该层负责与数据文件进行交互,包括读取和写入数据。在这个项目中,数据层对应的是Model层,负责与文件进行交互并提供数据的持久化存储。数据层可以定义接口,包括对话题和回帖数据的增删改查操作。具体实现可以使用文件读写操作来读取和写入数据。 -
逻辑层(
Service
):该层负责处理业务逻辑,对数据进行处理和操作。在这个项目中,逻辑层对应的是Entity层,负责处理话题和回帖的业务逻辑。逻辑层可以调用数据层的接口来进行数据的读取和写入,并对数据进行处理和操作,例如对话题和回帖进行增删改查的业务逻辑。 -
视图层(
Controller
):该层负责与用户交互,接收用户的请求并返回相应的结果。在这个项目中,视图层对应的是View层,负责处理用户请求和展示数据。视图层可以调用逻辑层的接口来处理用户请求,并将处理结果返回给用户。在这个项目中,视图层可以实现一个本地web服务,接收用户的请求并返回相应的话题内容和回帖列表。
这样的架构可以使代码更加清晰和可维护,各层之间的职责清晰,提高了代码的可扩展性和可测试性。同时,这种架构也符合MVC(Model-View-Controller)
的设计模式。
将会用到的工具
1.gin
(github.com/gin-gonic/gin
)框架
Gin是一个轻量级的Web框架,基于Go语言。它具有高性能、易用性和丰富的功能,被广泛用于构建Web应用程序和API服务。它具有以下特点。
-
快速:Gin框架是基于httprouter路由引擎的,具有高性能的路由功能,能够处理大量的并发请求。
-
轻量级:Gin框架的代码量相对较小,没有过多的依赖,启动和运行速度快。
-
路由功能:Gin框架提供了灵活且易用的路由功能,支持RESTful风格的路由,可以方便地定义路由规则和处理请求。
-
中间件支持:Gin框架支持中间件,可以在请求处理流程中添加各种功能,例如日志记录、身份验证、错误处理等。
-
参数绑定和验证:Gin框架提供了方便的参数绑定和验证功能,可以将请求参数绑定到结构体中,并进行验证,简化了参数处理的过程。
-
分组路由:Gin框架支持将路由进行分组,可以方便地对不同的路由进行分组管理,提高了代码的可读性和可维护性。
-
错误处理:Gin框架提供了统一的错误处理机制,可以方便地处理和返回错误信息。
-
渲染模板:Gin框架支持使用多种模板引擎进行页面渲染,例如HTML、JSON、XML等。
-
测试支持:Gin框架提供了方便的测试支持,可以进行单元测试和集成测试。
使用Gin框架可以快速搭建Web应用程序和API服务,它具有良好的性能和易用性,适用于各种规模的项目。同时,Gin框架的文档和社区资源也非常丰富,可以方便地获取帮助和解决问题。
go mod
管理工具
此前的文章中已经有做过介绍,详细内容请查看Go语言入门:使用依赖管理,站在巨人的肩膀之上|青训营 - 掘金 (juejin.cn)。
项目实践
数据处理与查询
从文件中将内容初始化到内存Map
当加载数据时,可以将数据从文件中读取到内存中的Map数据结构中,并以id为索引。以下是针对话题(Topic)和回帖(Post)的两套初始化代码:
- 初始化话题(Topic)数据:
// TopicRepository 结构体
type TopicRepository struct {
Topics map[int]Topic
}
// 初始化函数,从文件中加载数据到内存中的Map
func NewTopicRepository(filepath string) (*TopicRepository, error) {
// 读取文件内容
data, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, err
}
// 解析文件内容为Topic列表
var topics []Topic
err = json.Unmarshal(data, &topics)
if err != nil {
return nil, err
}
// 创建TopicRepository对象
topicRepo := &TopicRepository{
Topics: make(map[int]Topic),
}
// 将Topic列表添加到Map中,以id为索引
for _, topic := range topics {
topicRepo.Topics[topic.ID] = topic
}
return topicRepo, nil
}
- 初始化回帖(Post)数据:
// PostRepository 结构体
type PostRepository struct {
Posts map[int]Post
}
// 初始化函数,从文件中加载数据到内存中的Map
func NewPostRepository(filepath string) (*PostRepository, error) {
// 读取文件内容
data, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, err
}
// 解析文件内容为Post列表
var posts []Post
err = json.Unmarshal(data, &posts)
if err != nil {
return nil, err
}
// 创建PostRepository对象
postRepo := &PostRepository{
Posts: make(map[int]Post),
}
// 将Post列表添加到Map中,以id为索引
for _, post := range posts {
postRepo.Posts[post.ID] = post
}
return postRepo, nil
}
上述代码中,通过ioutil.ReadFile
函数从文件中读取数据,并使用json.Unmarshal
函数将数据解析为对应的结构体列表。然后,通过循环遍历结构体列表,将数据添加到内存中的Map中,以id作为索引。最后,将Map作为Repository对象的字段返回。这样就完成了从文件中加载数据到内存Map的初始化过程。
通过指定ID查询对应的话题/回帖
在Repository中添加查询方法可以方便地通过传入ID来获取对应的Topic或Post,并且支持通过Topic ID来查询该Topic下的所有Post。以下是针对话题(Topic)和回帖(Post)的查询方法函数:
- 话题(Topic)查询方法:
// TopicRepository 结构体
type TopicRepository struct {
Topics map[int]Topic
}
// 通过ID查询话题
func (repo *TopicRepository) GetTopicByID(id int) (Topic, error) {
topic, ok := repo.Topics[id]
if !ok {
return Topic{}, fmt.Errorf("topic not found")
}
return topic, nil
}
- 回帖(Post)查询方法:
// PostRepository 结构体
type PostRepository struct {
Posts map[int]Post
}
// 通过ID查询回帖
func (repo *PostRepository) GetPostByID(id int) (Post, error) {
post, ok := repo.Posts[id]
if !ok {
return Post{}, fmt.Errorf("post not found")
}
return post, nil
}
// 通过TopicID查询该Topic下的所有回帖
func (repo *PostRepository) GetPostsByTopicID(topicID int) []Post {
var posts []Post
for _, post := range repo.Posts {
if post.TopicID == topicID {
posts = append(posts, post)
}
}
return posts
}
上述代码中,通过在Repository结构体中添加查询方法,可以方便地根据ID获取对应的Topic或Post。在GetTopicByID方法中,通过传入的ID在内存的Map中查找对应的话题,如果找到则返回该话题,否则返回错误。在GetPostByID方法中也是类似的逻辑。而GetPostsByTopicID方法则是通过传入的TopicID,在内存的Map中遍历所有回帖,将属于该TopicID的回帖添加到一个新的切片中,并返回该切片作为结果。
服务(Service)
为了向用户返回数据,我们可以创建一个名为PageInfo的结构体,该结构体包含Topic和Posts字段。以下是PageInfo实体的代码:
// PageInfo 结构体
type PageInfo struct {
Topic Topic `json:"topic"`
Posts []Post `json:"posts"`
}
为了更完整的向客户端传递内容,我们还应包含状态码等内容。我们可以创建一个名为Response的结构体,该结构体包含一个Status字段、一个Message字段和一个PageInfo字段。以下是对应的代码片段:
// Response 结构体
type Response struct {
Status int `json:"status"`
Message string `json:"message"`
PageInfo PageInfo `json:"pageInfo"`
}
在上述代码中,Response结构体包含了一个Status字段和一个Message字段,用于表示请求的状态码和请求的信息。PageInfo字段是一个PageInfo类型的对象,表示页面内容。通过给结构体字段添加json
标签,可以在进行JSON序列化和反序列化时指定字段的名称。
有了Response结构体,我们可以在Service中使用它来表示包含请求状态码、请求信息和页面内容的响应。在需要进行JSON序列化和反序列化时,可以方便地将Response对象转换为JSON格式的数据或从JSON数据转换为Response对象。
接下来我们便可以开始实现查询话题Topic并返回数据的最终部分了。为了实现生成Response并返回的函数,我们可以创建一个名为GetPageInfo的函数,该函数接收一个topicID作为参数,并返回一个Response对象。在函数内部,我们可以先从全局共享的topic和post的repository实例中查询对应的Topic和其下的所有Post,然后将它们组装成PageInfo对象,并最终构建一个Response对象返回。以下是对应的代码:
// GetPageInfo 函数
func GetPageInfo(topicID int) Response {
// 从全局共享的topic和post的repository实例中查询对应的Topic和其下的所有Post
topic, err := topicRepo.GetTopicByID(topicID)
if err != nil {
return Response{
Status: http.StatusNotFound,
Message: "Topic not found",
}
}
posts := postRepo.GetPostsByTopicID(topicID)
// 组装PageInfo对象
pageInfo := PageInfo{
Topic: topic,
Posts: posts,
}
// 构建Response对象并返回
return Response{
Status: http.StatusOK,
Message: "Success",
PageInfo: pageInfo,
}
}
在上述代码中,我们首先通过全局共享的topic和post的repository实例分别查询对应的Topic和其下的所有Post。如果查询失败,我们返回一个包含404状态码和"Topic not found"信息的Response对象。如果查询成功,我们将查询到的Topic和Post组装成PageInfo对象。最后,我们构建一个包含200状态码、"Success"信息和PageInfo对象的Response对象,并返回它。
请注意,上述代码中的topicRepo和postRepo是全局共享的repository实例,你可以在初始化时将它们作为参数传入GetPageInfo函数,或者使用全局变量来引用它们。根据你的具体实现方式,你可能需要调整代码中的topicRepo和postRepo的引用方式。
路由设置
我们可以使用gin的方法来定义路由和处理函数。以下是对应的代码片段:
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建gin实例
r := gin.Default()
// 定义路由和处理函数
r.GET("/topics/:id", func(c *gin.Context) {
// 获取URL参数中的topicID
topicID := c.Param("id")
// 将topicID转换为整数
topicIDInt, err := strconv.Atoi(topicID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid topic ID"})
return
}
// 调用GetPageInfo函数获取Response对象
response := GetPageInfo(topicIDInt)
// 返回JSON响应
c.JSON(response.Status, response)
})
// 启动服务器
r.Run(":8080")
}
在上述代码中,我们首先导入了gin包,并创建了一个默认的gin实例。然后,我们使用r.GET
方法来定义了一个GET请求的路由,该路由匹配的路径为/topics/:id
,其中:id
是一个动态路由参数,表示topicID。在处理函数中,我们首先从URL参数中获取topicID,并将其转换为整数。然后,我们调用GetPageInfo
函数来获取Response对象。最后,我们使用c.JSON
方法将Response对象以JSON格式返回给客户端。
最后,我们使用r.Run(":8080")
来启动服务器,监听在8080端口上。
请注意,上述代码中的GetPageInfo
函数是前面提到的生成Response并返回的函数,你需要将其放在同一个文件中,并确保可以正确引用。