实践选题: GO语言工程实践课后作业:实现思路、代码以及路径记录;
基础复现
需求描述
- 展示话题和回帖列表
- 不考虑前端实现,只实现本地Web服务
- 话题和回帖数据使用文件存储
用例说明
通过ER图来标识,主要有两个实体Topic和Post,其中Topic包含属性id,title,content和date而Post包括id,topic_id,content和date
组件工具
- Gin 高性能goWeb框架
- Go Mod 使用以下指令:
go mod init+go get gopkg.in/gin-gonic/gin.v1@v1.3.0,指令在go.mod文件的路径下执行,若出现网络错误,则执行go env -w GOPROXY=https://goproxy.cn来使用国内代理
功能
- 通过话题ID查询话题信息
- 通过话题ID查询所有该话题下的ID
可以通过全扫描查询,为优化效率,使用索引
功能分层
本项目总体分为三层,分别为数据层repository,服务层service和控制层controller
- 数据层:本层为最底层,定义Topic结构体,Post结构体,索引等,根据传入路径读取信息并初始化话题索引和帖子列表索引,根据话题ID查询话题信息和帖子列表
- 服务层:借助数据层提供的服务实现本层服务,在本次定义页面信息结构体和信息流结构体(用于在流程中暂存数据),通过操作信息流对象,检查传入话题ID是否合法,根据话题ID查询话题信息和帖子列表,使用信息流将查询到的数据打包为页面信息
- 控制层:借助服务层提供的服务实现数据查询和打包,定义PageData结构体,包括状态码,状态信息,页面数据等信息,根据传入的话题ID字符串查询数据,并将数据和状态信息打包发送
代码开发
repository
- 在查询时,通过postDao结构体实现单例,通过单例查询体高查询效率
代码地址:repository.go
service
代码地址:service.go
controller
代码地址:controller.go
测试运行
主函数代码:main.go
核心代码解释:
r := gin.Default(): 这行代码创建了一个Gin的默认引擎实例,该实例用于处理HTTP请求和路由。r.GET("/topic/:topicId", func(c *gin.Context) { ... }): 这是一个路由定义,它指定了一个处理GET请求的路由。"/topic/:topicId"是一个路由路径,其中:topicId表示一个参数占位符,它允许在该位置匹配任何非空字符串。例如,如果请求的URL为/topic/123,那么123将被作为topicId的值传递给后面的处理函数。func(c *gin.Context) { ... }: 这是一个匿名函数(也称为闭包),是真正处理HTTP请求的代码块。在Gin框架中,所有的请求处理函数都需要具有这样的签名,即接受一个*gin.Context类型的参数。gin.Context对象封装了该HTTP请求的所有信息,并提供了许多有用的方法来处理请求和构建响应。topicId := c.Param("topicId"): 这行代码从请求中获取URL参数topicId的值,并将其存储在一个本地变量topicId中。c.Param()方法用于提取URL中的参数值。data := QueryPageInfo(topicId): 这行代码调用一个自定义的函数QueryPageInfo(),并将之前获取的topicId作为参数传递给该函数。QueryPageInfo()函数用于根据提供的topicId查询相应的页面信息。c.JSON(200, data): 这行代码使用c.JSON()方法将处理函数的返回数据data以JSON格式作为HTTP响应返回给客户端。在这里,HTTP状态码设置为200,表示请求成功。
可能的问题及解决:
- 若无法找到包 gin,可以在go.mod文件所在目录下执行控制台命令:
go get -u github.com/gin-gonic/gin即可自动下载并导入gin - 运行后通过浏览器打开:http://localhost:8080/topic/1,即可查询话题ID为1的话题信息和下属所有帖子信息
作业实现
发布帖子
为了发布一个帖子,需要一个前端界面来编写并发送帖子信息,使用ajax收发报文,script脚本如下所示:
- 通过输入框获取发布帖子的话题ID和内容
- 通过Date().getTime()获取提交表单时的时间作为发帖时间
$(document).ready(function () {
// 监听表单提交事件
$("#loginForm").submit(function (event) {
event.preventDefault(); // 阻止表单的默认提交行为
var topicId = $("#topicId").val();
var postContent = $("#postContent").val();
var time = new Date().getTime();
console.log(topicId + " " + postContent);
var postData = {
topicId: topicId,
content: postContent,
create_time: time
};
$.ajax({
type: "POST",
url: "http://localhost:8080/post", // 替换为实际的后台处理URL
data: postData,
success: function (response) {
console.log("Success" + response);
if (response == 'success') {
alert("发帖成功")
} else {
alert("发帖失败");
}
},
error: function (error) {
console.log("Error:" + error);
}
});
});
});
页面预览:

设置静态文件目录,防止出现跨域,HTML文件目录为工程目录下的assets目录中
r.Static("/assets", "../assets")
此时浏览器打开http://localhost:8080/assets/post.html即可打开post.html文件
服务器需要接受服务器发送的信息,因此需要为添加POST方法,用于接收数据并处理;通过c.PostForm获取请求报文中相应字段的内容,并调用下层方法进行下一步处理,处理成功后返回success信息
r.POST("post", func(c *gin.Context) {
topicId := c.PostForm("topicId")
content := c.PostForm("content")
create_time := c.PostForm("create_time")
//使用协程进行添加,防止阻塞
go func() {
err := packPostInfo(topicId, content, create_time)
if err != nil {
return
}
}()
println("接收到的帖子信息为:", topicId, " ", content, " ", create_time)
c.JSON(200, "success")
})
HTML文件代码:post.html
控制层方法
在控制层中对信息进行初步处理,将传入的字符串类型的话题ID和时间转为int64类型,并为Post类型对象,然后由服务层进行下一步处理.
- 使用协程来执行发帖操作,防止阻塞,优化效率
func packPostInfo(topicId string, content string, create_time string) error {
//将传入的字符串id转换为int64
topicId_int, err := strconv.ParseInt(topicId, 10, 64)
if err != nil {
println(err.Error())
return err
}
//将时间字符串转为int64
create_time_int, err := strconv.ParseInt(create_time, 10, 64)
if err != nil {
println(err.Error())
return err
}
post := &Post{
TopicId: topicId_int,
Content: content,
Date: create_time_int,
}
//添加帖子ID(服务层方法)并进行下一步操作
err = paperPostInfo(post)
if err != nil {
println(err.Error())
return err
}
return nil
}
服务层
在服务层中,使用单例执行添加操作
func paperPostInfo(post *Post) error {
err := NewPostDaoInstance().addPost(post)
if err != nil {
return err
}
return nil
}
数据层
在数据层方法中为该帖子添加帖子ID,然后将帖子添加到帖子索引中,实现发帖
func (p *PostDao) addPost(post *Post) error {
//更新最大帖子ID
maxPostId++
post.Id = maxPostId
//将帖子放入对应话题下的列表中,更新索引
//更新索引时加锁,防止多线程同时写入
indexLock.Lock()
postIndexMap[post.TopicId] = append(postIndexMap[post.TopicId], post)
indexLock.Unlock()
println("成功添加帖子:", "topicId:", post.TopicId, "content:", post.Content, "date:", post.Date)
return nil
}
本地ID生成唯一
为了使本地生成的ID唯一,使用自增ID的方法,在数据层中维护一个maxPostId对象,在初始化索引时找到已存在的最大帖子ID,在发布帖子时,使maxPostId增1后作为新帖子的ID,以保证本地ID生成唯一
更新索引
由于处理POST请求使用协程,可能会出现并发问题,因此在向索引表中添加内容前,为索引表加锁,添加完成后释放锁,以避免并发冲突,需要在原索引对象中添加锁
var (
topicIndexMap map[int64]*Topic
postIndexMap map[int64][]*Post
indexLock sync.Mutex
)
总结
本次在原有项目基础上实现了发帖功能,熟练了对gin框架服务器的构建和使用,也对Go语言并发处理,服务流程设计有了初步认识,对处理Go项目中出现的各种问题积累了经验.
可能的问题
- 导入自定义包
在GoLand下导入自定义包,包应放置在项目目录下的src目录中,要保证包中函数能被包外调用,需要将函数首字母大写.
若向main包中导入src中的porject_hw包,需要在main包的go.mod中添加:require project1_hw v0.0.0以及replace project1_hw => ../src/project1_hw,其中 replace后的参数为包名, =>后的参数为包所在的相对路径
然后在文件中添加 import "project1_hw"即可导入该包