社区话题服务端课后实践|青训营

75 阅读6分钟

需求描述

  • 展示话题和回帖列表

  • 暂不考虑前端页面实现,仅仅实现一个本地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
	}
}

展示结果如下:

image.png