Go语言工程实践——实现简易论坛服务端
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来测试接口,可以看到两个接口都可以正常工作
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!