Go语言工程实践——实现简易论坛服务端

发布时间 2023-08-11 14:56:10作者: Nemuzuki

Go语言工程实践——实现简易论坛服务端

这篇文章是在我完成极简版抖音项目之后撰写的,相比刚听网课的时候的懵懵懂懂,书写过一些go代码后对这个作业的理解深入了很多。这个作业是实现一个服务端go程序,下面从零开始开发。

准备工作

首先建立一个名为server_demo的空目录作为项目根目录,然后使用go mod init server_demo初始化一个server_demo模块,此时会发现自动生成一个go.mod文件,该文件是模块的配置文件,此时里面只有模块名和go版本

 module server_demo
 
 go 1.12

之后,我们需要导入go的web框架gin,执行go get github.com/gin-gonic/gin,go.mod会多出一行,表示该模块依赖的模块(包)及其版本

 require github.com/gin-gonic/gin v1.9.1 // indirect

还会生成一个go.sum文件,包含了每个包的名称、版本和校验和信息

 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=

项目描述

项目背景:实现一个论坛,论坛里有很多Topic(话题),每个话题下有若干Post(帖子)。要实现的业务:根据topic id查询一个话题的名称,根据topic id查询一个话题下的所有帖子

根据项目分层原则,可以将代码分成如下目录:

  • models目录下定义Topic、Post以及数据在内存中存放的数据结构
  • dao目录下实现业务所需的原子操作,主要是对数据库的初始化和增删改查
  • controller目录下实现服务接口
  • routes目录下定义接口路由
  • 主目录下main.go中写主函数

项目结构如下

 .                  
 ├── controller     
 │   ├── bmi.go     
 │   └── query.go   
 ├── dao            
 │   └── dao.go     
 ├── data           
 │   ├── post       
 │   └── topic      
 ├── go.mod         
 ├── go.sum         
 ├── main.go        
 ├── models         
 │   ├── post.go    
 │   └── topic.go   
 ├── routes         
 │   ├── api.go     
 │   └── route.go   
 ├── server_demo.exe

下面将分层阐述具体实现方法

models

首先根据data中的数据格式,使用https://oktools.net/json2go工具将json转为go结构体

 //Post
 {"id":1,"parent_id":1,"content":"小姐姐快来1","create_time":1650437616,"user_id":1}
 {"id":2,"parent_id":1,"content":"小姐姐快来2","create_time":1650437617,"user_id":2}
 {"id":3,"parent_id":1,"content":"小姐姐快来3","create_time":1650437618,"user_id":13}
 {"id":5,"parent_id":1,"content":"测试内容嗨嗨嗨嗨","create_time":1652073758,"user_id":2}
 
 //Topic
 {"id":1,"title":"青训营来啦!","content":"小姐姐,快到碗里来~","create_time":1650437625}
 {"id":2,"title":"青训营来啦!","content":"小哥哥,快到碗里来~","create_time":1650437640}

在Post.go定义Post结构体,其中parent id就是topic id

 package models
 
 type Post struct{
     ID int `json:"id"`
     ParentID int `json:"parent_id"`
     Content string `json:"content"`
     CreateTime int `json:"create_time"`
     UserID int `json:"user_id"`
 }

在Topic.go定义Topic结构体,每个topic有一个标题title和一个解释内容content

 package models
 
 type Topic struct{
     ID int `json:"id"`
     Title string `json:"title"`
     Content string `json:"content"`
     CreateTime int `json:"create_time"`
 }

dao

dao.go定义数据存放的结构,存储Post的数据结构postIndexMap,存储Topic的数据结构topicIndexMap。postIndexMap是一个将topic id映射到Post列表的Map,每个列表中是一个话题下的帖子。topicIndexMap是一个将topic id映射到Topic的Map

 var (
     postIndexMap  map[int32][]*models.Post
     topicIndexMap map[int32]*models.Topic
 )

根据data内容对这两个存储结构初始化。InitTopicIndexMap读取data中的数据存入topicIndexMap中,InitPostIndexMap将数据存入postTmpMap中,要注意的是这两个函数必须首字母大写,这样才能被外部模块调用(go特性)

 //初始化topic map
 func InitTopicIndexMap() error {
     open, err := os.Open("./data/topic") //相对路径,从根目录开始算
     if err != nil {
         return err
     }
     topicTmpMap := make(map[int32]*models.Topic)
     scanner := bufio.NewScanner(open)
     for scanner.Scan() {
         text := scanner.Text()
         var topic models.Topic
         if err := json.Unmarshal([]byte(text), &topic); err != nil {
             return err
         }
         topicTmpMap[int32(topic.ID)] = &topic
     }
     topicIndexMap = topicTmpMap
     return nil
 }
 
 //初始化post map
 func InitPostIndexMap() error {
     open, err := os.Open("./data/post")
     if err != nil {
         return err
     }
     postTmpMap := make(map[int32][]*models.Post)
     scanner := bufio.NewScanner(open)
     for scanner.Scan() {
         text := scanner.Text()
         var post models.Post
         if err := json.Unmarshal([]byte(text), &post); err != nil {
             return err
         }
         parentId := int32(post.ParentID)
         _, ok := postTmpMap[parentId]
         if !ok {
             postTmpMap[parentId] = []*models.Post{}
         }
         postTmpMap[parentId] = append(postTmpMap[parentId], &post)
 
     }
     postIndexMap = postTmpMap
     return nil
 }

之后定义从两个Map中读取数据的函数

 //根据topic id查询topic。注意函数首字母大写才能被别的模块调用
 func QueryTopicById(id int32) *models.Topic {
     return topicIndexMap[id]
 }
 
 //根据请求的topic id查询post
 func QueryPostByParentId(parentID int32) []*models.Post {
     return postIndexMap[parentID]
 }

controller

query.go中编写接口,即收到请求的处理过程。QueryTopicById根据请求的topic id获取topic内容,首先调用dao.QueryTopicById查找到Topic,然后读取里面的各个属性,使用c.JSON来写响应体。QueryPostById也是同理,响应一个topic下的所有post信息,返回一个列表

 //根据请求的topic id获取topic内容
 func QueryTopicById(c *gin.Context) {
     id, _ := strconv.ParseInt(c.Query("id"), 10, 8)
     topic := dao.QueryTopicById(int32(id))
     c.JSON(http.StatusOK, gin.H{
         "status_code": http.StatusOK,
         "status_msg":  "查询标题成功",
         "comment": models.Topic{
             ID:         int(id),
             Title:      topic.Title,
             Content:    topic.Content,
             CreateTime: topic.CreateTime,
         },
     })
 }
 
 //根据请求的topic id和post id获取post内容
 func QueryPostById(c *gin.Context) {
     parentId, _ := strconv.ParseInt(c.Query("parent_id"), 10, 8)
     postList := dao.QueryPostByParentId(int32(parentId))
     c.JSON(http.StatusOK, gin.H{
         "status_code": http.StatusOK,
         "status_msg":  "查询帖子成功",
         "post_list":   postList,
     })
 }
 

routes

api.go中定义上面实现的两个接口的路由,比如我们要调用controller.QueryTopicById接口,那么URL就是127.0.0.1:8080/server_demo/getTopic/

 func ApiRoutes(r *gin.Engine) {
 
     apiRouter := r.Group("server_demo")
 
     apiRouter.GET("/getTopic/", controller.QueryTopicById)
     apiRouter.GET("/getPost/", controller.QueryPostById)
 }

route.go中对路由初始化,包括三个过程

  • 注册全局中间件:日志、发生panic时恢复程序运行两个功能
  • 注册 API 路由
  • 配置 404 请求
 // SetupRoute 路由初始化
 func SetupRoute(router *gin.Engine) {
 
     // 注册全局中间件
     registerGlobalMiddleWare(router)
 
     // 注册 API 路由
     ApiRoutes(router)
 
     // 配置 404 请求
     setup404Handler(router)
 }
 
 func registerGlobalMiddleWare(router *gin.Engine) {
     router.Use(
         gin.Logger(),//日志中间件
         gin.Recovery(),//发生panic时恢复程序运行
     )
 }
 
 // 处理404请求
 func setup404Handler(router *gin.Engine) {
     router.NoRoute(func(c *gin.Context) {
         c.JSON(http.StatusNotFound, gin.H{
             "error_code":    404,
             "error_message": "路由未定义,请确认 url 和请求方法是否正确。",
         })
     })
 }

main

main.go

  • 初始化数据库
  • 配置路由
  • 启动整个服务,绑定在8080端口上
 func main() {
     //初始化数据库
     dao.InitPostIndexMap()
     dao.InitTopicIndexMap()
     //配置路由
     router := gin.New()
     routes.SetupRoute(router)
     //启动
     err := router.Run(":8080")
     if err != nil {
         fmt.Println(err.Error())
     }
 }

测试

运行方法

go build
./server_demo.exe

可以使用Postman来测试接口,可以看到两个接口都可以正常工作

image.png

image.png