Go语言实战:一个简单的社区Web服务|青训营

182 阅读10分钟

需求描述

社区话题页面

  1. 展示话题(标题,文宇描述)和回帖列表
  2. 暂不考虑前端页面实现,仅仅实现一个本地web服务
  3. 话题和回帖数据用文件存储

该需求涉及到的用例

用户浏览社区页面,该页面包含话题的内容以及回帖列表。

需要用到的实体类

话题(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序列化和反序列化中使用。

层级结构

这个项目将按照以下三层架构进行设计和实现:

  1. 数据层(Repository):该层负责与数据文件进行交互,包括读取和写入数据。在这个项目中,数据层对应的是Model层,负责与文件进行交互并提供数据的持久化存储。数据层可以定义接口,包括对话题和回帖数据的增删改查操作。具体实现可以使用文件读写操作来读取和写入数据。

  2. 逻辑层(Service):该层负责处理业务逻辑,对数据进行处理和操作。在这个项目中,逻辑层对应的是Entity层,负责处理话题和回帖的业务逻辑。逻辑层可以调用数据层的接口来进行数据的读取和写入,并对数据进行处理和操作,例如对话题和回帖进行增删改查的业务逻辑。

  3. 视图层(Controller):该层负责与用户交互,接收用户的请求并返回相应的结果。在这个项目中,视图层对应的是View层,负责处理用户请求和展示数据。视图层可以调用逻辑层的接口来处理用户请求,并将处理结果返回给用户。在这个项目中,视图层可以实现一个本地web服务,接收用户的请求并返回相应的话题内容和回帖列表。

这样的架构可以使代码更加清晰和可维护,各层之间的职责清晰,提高了代码的可扩展性和可测试性。同时,这种架构也符合MVC(Model-View-Controller)的设计模式。

将会用到的工具

1.gingithub.com/gin-gonic/gin)框架

Gin是一个轻量级的Web框架,基于Go语言。它具有高性能、易用性和丰富的功能,被广泛用于构建Web应用程序和API服务。它具有以下特点。

  1. 快速:Gin框架是基于httprouter路由引擎的,具有高性能的路由功能,能够处理大量的并发请求。

  2. 轻量级:Gin框架的代码量相对较小,没有过多的依赖,启动和运行速度快。

  3. 路由功能:Gin框架提供了灵活且易用的路由功能,支持RESTful风格的路由,可以方便地定义路由规则和处理请求。

  4. 中间件支持:Gin框架支持中间件,可以在请求处理流程中添加各种功能,例如日志记录、身份验证、错误处理等。

  5. 参数绑定和验证:Gin框架提供了方便的参数绑定和验证功能,可以将请求参数绑定到结构体中,并进行验证,简化了参数处理的过程。

  6. 分组路由:Gin框架支持将路由进行分组,可以方便地对不同的路由进行分组管理,提高了代码的可读性和可维护性。

  7. 错误处理:Gin框架提供了统一的错误处理机制,可以方便地处理和返回错误信息。

  8. 渲染模板:Gin框架支持使用多种模板引擎进行页面渲染,例如HTML、JSON、XML等。

  9. 测试支持:Gin框架提供了方便的测试支持,可以进行单元测试和集成测试。

使用Gin框架可以快速搭建Web应用程序和API服务,它具有良好的性能和易用性,适用于各种规模的项目。同时,Gin框架的文档和社区资源也非常丰富,可以方便地获取帮助和解决问题。

  1. go mod管理工具

此前的文章中已经有做过介绍,详细内容请查看Go语言入门:使用依赖管理,站在巨人的肩膀之上|青训营 - 掘金 (juejin.cn)

项目实践

数据处理与查询

从文件中将内容初始化到内存Map

当加载数据时,可以将数据从文件中读取到内存中的Map数据结构中,并以id为索引。以下是针对话题(Topic)和回帖(Post)的两套初始化代码:

  1. 初始化话题(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
}
  1. 初始化回帖(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)的查询方法函数:

  1. 话题(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
}
  1. 回帖(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并返回的函数,你需要将其放在同一个文件中,并确保可以正确引用。