需求描述
-
展示话题和回帖列表
-
暂不考虑前端页面实现,仅仅实现一个本地web服务
-
话题和回帖数据用文件存储
课后需要实现的内容为 支持发布帖子,本地id生成需要保证不重复,唯一性 append文件,更新索引
组件工具
针对web服务,采用Go中已有的Gin框架实现,Gin 是一个用于构建 Web 应用程序和 API 的 Go 语言框架。它提供了高性能、易用的路由、中间件和其他功能,使得开发 Web 服务变得更加简单和高效。 Gin 是一个轻量级的框架,旨在快速地处理 HTTP 请求和响应。它的设计目标是提供最小的内存占用和高度优化的性能,因此在处理高并发的情况下表现出色。如果要使用的话需要import "github.com/gin-gonic/gin"
GitHub知识
针对课程源码中的GitHub库的使用上,有一些小的知识点,需要花时间去了解,对于源码中,为什么如何使用自己的GitHub包呢,首先,我们需要对go.mod中的module进行重新设置,需要更改成自己的GitHub的库地址例如下面这样:
module github.com/Mrmengqiushisan/go_test,下一步需要通过将远端GitHub上的库进行发布并附带标签,这样我们在本地就可以随意访问我们自己的GitHub包了,例如这样import "github.com/Mrmengqiushisan/go_test/repository"
项目结构
想要实现整个功能,我们需要对该项目的结构进行分层,抽象模型的考虑上,可以分为两个:post,topic,通过两个结构体可以很好的实现,对于这两个类的如何想要实现单例模式的话,C++角度需要利用语法的机制做很多调整,但对于Go语言来说通过postOnce sync.Once即可实现,sync.Once 是 Go 语言标准库 sync 包中的一个结构体类型,用于实现只执行一次的操作。它通常用于确保在并发环境下某个函数只被执行一次,无论有多少个 goroutine 在同时调用该函数。
sync.Once 提供了一个 Do 方法,用于指定只执行一次的操作。该方法接收一个函数作为参数,当多个 goroutine 同时调用 Do 方法时,只有一个 goroutine 会执行传入的函数,其他 goroutine 将会等待执行结束。当第一个 goroutine 执行结束后,done 字段会被标记为已完成,后续的调用将不再执行传入的函数。所以是线程安全的,实现方式可以参考如下代码:
package repository
import "sync"
type Post struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type PostDao struct {}
var (
postDao *PostDao
postOnce sync.Once
)
func NewPostDaoInstance() *PostDao {
postOnce.Do(
func() {
postDao = &PostDao{}
})
return postDao
}
func (*PostDao) QueryPostsByParentId(parentid int64) []*Post {
return postIndexMap[parentid]
}
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type TopicDao struct{}
var (
topicDao *TopicDao
topicOnce sync.Once
)
func NewTopicDaoInstance() *TopicDao {
topicOnce.Do(
func() {
topicDao = &TopicDao{}
})
return topicDao
}
func (*TopicDao) QueryTopicById(id int64) *Topic {
return topicIndexMap[id]
}
Map表的初始化创建
考虑到对帖子的更新修正以及及时访问,我们需要维护两个变量用来记录文件中已有的帖子数量,针对InitTopicIndexMap InitPostIndexMap这两个函数执行之前需要对该Map进行清除,以便于重新读写数据,同样也需要对文件数id进行置空,针对与这部分的实现主要有以下几个关键点:
- 文件访问的函数使用
os.open - 拿到文件handle后如何通过
bufio.newScanner实现文件的行遍历 - map的处理行post和topic有所区别,因为post的map索引也是通过话题id来实现的,所以对于帖子的存储上我们需要通过切片方式进行存储,这样可以使用
append方式进行追加
if len(postIndexMap) > 0 {
postIndexMap = make(map[int64][]*Post)
postId = 0
}
if len(topicIndexMap) > 0 {
topicIndexMap = make(map[int64]*Topic)
topicId = 0
}
serivce设计
页面的设计上我们一般是一个话题以及追加一系列的帖子,这样需要设计的结构体上就可以设计成这样:
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
这个结构体的设计已经可以满足界面设计需求了,那为什么还需要设计一个更加复杂的呢,首先考虑到界面索引的问题我们需要确定话题ID,根据话题ID需要确定帖子的Map索引数据,索引查找过程为两个没有关联的过程可以开启协程处理,这样的这个结构体的设计是很有必要的
type QueryPageInfoFlow struct {
topicId int64
pageInfo *PageInfo
topic *repository.Topic
posts []*repository.Post
}
其他的设计同源码,相对而言是较简单的
Controller设计
通过封装好的service.QueryPageInfo(topicId)实现页面数据的获取,结构体的设计如下:
type PageData struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
在 Go 语言中,interface{} 是一种特殊的数据类型,被称为空接口(Empty Interface)。空接口可以表示任意类型的值,因为它不包含任何方法,所以它对所有类型都是兼容的。
在 Go 中,每个类型都实现了空接口,因为它不要求任何方法。因此,你可以将任意值赋给空接口变量,也可以从空接口中获取任意值。
server.go设计
此部分需要开启gin框架,在此之前需要进行Init两个map表,为了实现发布帖子的功能,我们需要设计一个新的函数
InputPost()这个函数可以在文件中添加新的帖子,设计如下:
func InputPost() error {
for {
fmt.Println("结束请输入E")
fmt.Println("我需要以下信息:")
fmt.Println("请输入发布帖子的内容:")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input Please try again", err)
continue
}
topicContext := strings.Trim(input, "\r\n")
if strings.ToUpper(topicContext) == "E" {
break
}
fmt.Println("请输入您选择的话题:")
reader = bufio.NewReader(os.Stdin)
input, err = reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input Please try again", err)
continue
}
parentIdStr := strings.Trim(input, "\r\n")
parent, err := strconv.ParseInt(parentIdStr, 10, 64)
if err != nil {
fmt.Println("Invalid input,Please enter an integer value")
continue
} //构建帖子结构
now := time.Now()
id := repository.GetPostCount()
(*id)++
fmt.Println("当前id值为")
posTmp := repository.Post{
Id: *id,
ParentId: parent,
Content: topicContext,
CreateTime: now.Unix(),
}
open, err := os.OpenFile("./data/post", os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Println("file can not open ")
return errors.New("file can not open")
}
buf, err := json.Marshal(posTmp)
if err != nil {
fmt.Println("序列化失败")
return err
}
_, err = open.Write([]byte{'\n'})
_, err = open.Write(buf)
if err != nil {
fmt.Println("写入失败", err)
return err
}
fmt.Println("数据写入成功")
}
return nil
}
对于main函数的修改,主要是GET的调用过程中,我们需要确认用户是否需要发布新的帖子到哪一个话题中,所以需要做一些小的修改
func main() {
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.GET("/community/page/get/:id", func(ctx *gin.Context) {
topicId := ctx.Param("id")
for {
fmt.Println("请问是否有帖子需要输入呢")
reader := bufio.NewReader(os.Stdin)
put, err := reader.ReadString('\n')
if err != nil {
fmt.Println("input error", err)
break
}
putstr := strings.Trim(put, "\r\n")
if strings.ToUpper(putstr) == "Y" {
if err := InputPost(); err != nil {
break
}
} else {
break
}
}
if err := Init("./data/"); err != nil {
fmt.Println("再次初始化失败", err)
}
data := cotroller.QueryPageInfo(topicId)
ctx.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
展示结果如下: