高质量编程与性能调优实战| 青训营

71 阅读14分钟

Go 语言进阶与依赖管理(有实战)

1.Go语言进阶

1.1并发VS并行

Go可以充分发挥多核优势,高效运行。

image-20230807173847855.png 并发:多线程程序在单核心的 cpu 上运行;

并行:多线程程序在多核心的 cpu 上运行。

总的来说,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。(时间片:CPU分配给各个程序的时间)

1.2 线程

线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

(而进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位)

一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。

image-20230807174458517.png 协程:用户态,轻量级线程,栈 MB 级别。 线程:内核态,线程跑多个协程,栈 KB 级别。

这里解释一下,协程不是进程,也不是线程,它就是一个可以在某个地方挂起的特殊函数,并且可以重新在挂起处继续运行

而我们的go语言开启协程也很简单,就是在函数的前面写上go关键字即可

func hello(i int) {
   println("helle :" + fmt.Sprint(i))
}

func main() {
   for i := 0; i < 5; i++ {
      go func(j int) {    //就是这个func前面的go
         hello(j)
      }(i)
   }
   time.Sleep(time.Second)//使用time.Sleep(time.Second)来阻塞主函数,来防止主函数退出进程
}

对于这个代码,我们可以看到输出的结果是乱序的,但是for循环是有序输出的,

image-20230807175033905.png 这个也可以代表,代码是通过并行来打印的输出。

1.2.1协程通信共享内存

go语言的最大两个亮点,一个是goroutine,一个就是channel了。在go中,提倡通过通信共享内存而不是通过共享内存而实现通信

Go 通过 channel 实现 CSP(通信顺序进程) 通信模型,主要用于 goroutine 之间的消息传递和事件通知。 有了 channel 和 goroutine 之后,Go 的并发编程变得异常容易和安全,得以让程序员把注意力留到业务上去,实现开发效率的提升。

image-20230807175357025.png

1.3Channel

Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。

它的操作符是箭头 <- ,箭头指示数据流向,箭头指向哪里,数据就流向哪里

image-20230807175642080.png

在channel中,容量表示通道最多可容纳的元素数,并表示通道的缓存大小,如果未设置容量,或容量设置为0,则表示通道没有缓存,只有当发送方和接收方准备就绪时,才会进行通信。

如果设置了缓存,则可能不会发生阻塞。只有当缓冲区已满时,发送才会阻止,而只有当缓存为空时,接收才会阻止。零信道无法通信。

make(chan type,[size])
  • [size]代表有无缓冲通道
make(chan int)//无缓冲通道
make(chan int,2)//有缓冲通道

下面就是一个通信实现共享内存的例子。

A子协程发送0~9数字

B子协程计算输入数字的平方

主协程输出最后的平方数

package main

import "fmt"

func main() {
// 创建两个通道
	src := make(chan int)
	dest := make(chan int, 3)
	go func() {   //协程A
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
			fmt.Println("src<-", i)
		}
	}()
	go func() {  //协程B
		defer close(dest)
		for i := range src { //有数据传就一直接收
			dest <- i * i
			fmt.Println("dest<-", i*i)
		}
	}()
     // 主协程
	for i := range dest { //有数据传就一直接收
		fmt.Println(i)
	}
}

image-20230807180623417.png

打印出子协程的值更方便理解~但是注意print函数会有部分延迟,所以会有点慢打印出。

1.4并发安全锁

可以通过下面一个例子来看一下加了锁和不加锁的区别。

package main

import (
	"sync"
	"time"
)

func main() {
	add()
}

var (
	x    int64
	lock sync.Mutex
)

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()
		x++
		lock.Unlock()
	}
}

func addWithoutLock() {
	for i := 0; i < 2000; i++ {
		x++
	}
}
func add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("使用锁:", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("不使用锁:", x)
}

输出:image-20230807181459491.png

可以看到在并发执行时候会导致数据的不稳定,在go语言中,我们可以使用WaitGroup来执行并发任务的同步。

1.5 WaitGroup

可以替代之前time.Sleep的操作,让主协程等待子协程全部执行完之后再结束

WaitGroup 类实现的功能是:等待一系列协程并发地执行完毕。如果不等待所有协程执行完毕,可能会导致一些线程安全问题。sync.WaitGroup 包含 3 个方法: 方法 作用

方法作用
Add(delta int)主协程调用该方法,设置 delta 为需要等待的协程数量,就是说计数器+delta
Done()每个子协程运行起来,当每个子协程执行结束后,调用 Done() 表示子协程运行结束, 计数器-1
Wait()当所有协程执行完毕后,代码块可使用 Wait() ,当 计数器 ==0时,才执行后续代码

我们对之前那个例子改造一下

package main

import (
  "fmt"
  "sync"
)

func hello(i int) {
  fmt.Println("routine:", i)
}

func main() {
  var wg sync.WaitGroup //+
  wg.Add(5)//+
  for i := 0; i < 5; i++ {
    go func(j int) {
      hello(j)
      wg.Done()
    }(i)
  }
  wg.Wait()
}

输出: image-20230807182721143.png 在这个过程中,就是首先通过add方法,对计数器+5,然后开启协程,每个协程执行完后,通过done对计数器减少1,最后wait主协程阻塞,计数器为0退出主协程。得到最终的输出结果。

2.依赖管理

2.1发展历程

Gopath->go vender->go module

2.1.1 Gopath

GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区。

image-20230807202314537.png

缺点:无法实现包的多版本控制

2.1.2 go vender

相当于保存多个依赖文件

image-20230807202709281.png 缺点:

底层包版本不同,就是说无法控制依赖的版本。更新项目又可能出现依赖冲突,导致编译出错。

2.1.3 Go Modoule

通过go.mod文件管理依赖包版本

通过go get/go mod指令工具管理依赖包

依赖管理三要素:

1.配置文件,描述依赖:go.mod

2.中心仓库管理依赖库:Proxy

3.本地工具:go get/mod

如果一个项目依赖了两个版本,对于go来说,会选择较高的兼容版本

3.项目实战

3.1实现功能:

社区话题页面

  • 展示话题(标题,文字描述)和回帖列表

  • 暂不考虑前端页面实现,仅仅实现一个本地web服务

  • 话题和回帖数据用文件存储

  • 支持发布帖子

  • 本地id生成要保证不重复

  • append文件 更新索引要注意Map的并发安全问题

从功能入手去一点点分析,用户浏览消费,涉及页面的展示,包括话题内容和回帖的列表,那我们就可以定义两个结构体

image-20230808221243987.png

3.2分层结构
  • 数据层:数据Model,外部数据的增删改查
  • 逻辑层:业务Entity,处理核心业务逻辑输出
  • 视图层:视图view,处理和外部的交互逻辑

image-20230809163050750.png

数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。 Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层; Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果, api形式访问就好,

3.3实现
1.数据层

这里我们用map实现内存索引,在服务对外暴露前,利用文件元数据初始化全局内存索引,这样就可以实现0(1)的时间复杂度查找操作。

var(
	topicIndexMap map[int64]*Topic
	postIndexMap map[int64][]*Post
)

下面是具体的实现,解释一下代码过程,里面也有语句注释,便于理解,首先是打开文件,基于file初始化scammer,通过迭代器方式遍历数据行,转化为结构体存储至内存map,这就是初始化话题内存索引

//init.go
package repository

import (
	"bufio"
	"encoding/json"
	"os"
)

// 使用索引数据结构提高查询速度
var (
	topicIndexMap map[int64]*Topic
	postIndexMap  map[int64][]*Post
)

// Init 函数用于初始化仓库,接收一个文件路径作为参数
func Init(filePath string) error {
	if err := initTopicIndexMap(filePath); err != nil {
		return err
	}
	if err := initPostIndexMap(filePath); err != nil {
		return err
	}
	return nil
}

// 初始化主题索引函数,接收一个文件路径作为参数
func initTopicIndexMap(filePath string) error {
	open, err := os.Open(filePath + "topic") //打开文件
	if err != nil {
		return err
	}
	defer open.Close() // 在函数结束时关闭文件

	// 创建一个扫描器来逐行读取文件内容
	scanner := bufio.NewScanner(open)
	topicTmpMap := make(map[int64]*Topic) // 创建一个临时map用于存储主题索引
	for scanner.Scan() {
		text := scanner.Text()
		var topic Topic
		// 将文本解析为 Topic 结构并存储到map中
		if err := json.Unmarshal([]byte(text), &topic); err != nil {
			return err
		}
		topicTmpMap[topic.Id] = &topic
	}
	topicIndexMap = topicTmpMap // 将临时map内容赋值给topic索引
	return nil
}

// post内容同理
func initPostIndexMap(filePath string) error {
	open, err := os.Open(filePath + "post")
	if err != nil {
		return err
	}

	defer open.Close()

	scanner := bufio.NewScanner(open)
	postTmpMap := make(map[int64][]*Post)
	for scanner.Scan() {
		text := scanner.Text()
		var post Post
		if err := json.Unmarshal([]byte(text), &post); err != nil {
			return err
		}
		posts, ok := postTmpMap[post.ParentId]
		if !ok {
			postTmpMap[post.ParentId] = []*Post{&post} //如果之前没有这个主题的帖子,就创建一个新的帖子切片,将当前的帖子放入其中,然后将这个切片与主题的父主题 ID 相关联。
			continue
		}
		posts = append(posts, &post) //将当前的帖子添加到之前已经存在的帖子分类中
		postTmpMap[post.ParentId] = posts
	}
	postIndexMap = postTmpMap
	return nil
}

有了内存索引,下一步就是实现查询操作就比较简单了,直接根据查询key获得map中的value就好了,这里用到了sync.once,主要适用高并发的场景下只执行一次的场景,这里的基于once的实现模式就是我们平常说的单例模式,减少存储的浪费。

//topic.go
package repository

import (
	"sync"
)

// 主题topic的结构体定义
type Topic struct {
	Id         int64  `json:"id"`
	Title      string `json:"title"`
	Content    string `json:"content"`
	CreateTime int64  `json:"create_time"`
}

// // TopicDao 结构是主题数据访问对象,用于管理主题信息的访问
type TopicDao struct {
}

var (
	topicDao  *TopicDao //用于存储 TopicDao 实例的变量
	topicOnce sync.Once // 用于保证只初始化一次的同步机制
)

// 创建并返回一个 TopicDao 实例
func NewTopicDaoInstance() *TopicDao {
	topicOnce.Do(
		func() {
			topicDao = &TopicDao{}
		})
	return topicDao
}

// QueryTopicById 根据主题 ID 查询并返回对应的主题信息
func (*TopicDao) QueryTopicById(id int64) *Topic {
	return topicIndexMap[id]
}

//post.go
package repository

import (
	"sync"
)

// 主题post的结构体定义
type Post struct {
	Id         int64  `json:"id"`
	ParentId   int64  `json:"parent_id"` //就是topic_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
}

// QueryPostById 根据主题 ID 查询对应的帖子信息
func (*PostDao) QueryPostById(parentId int64) []*Post {
	return postIndexMap[parentId] //// 返回与给定主题 ID 相关的帖子列表
}

2.逻辑层

有了数据层之后,我们就开始实现service(逻辑)层,首先我们需要先定义一下service实体

type PageInfo struct {
	Topic    *repository.Topic
	PostList []*repository.Post
}

流程呢就是参数校验→准备数据→组装实体

下面是代码流程编排

func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
	if err := f.checkParam(); err != nil {
		return nil, err
	}
	if err := f.prepareInfo(); err != nil {
		return nil, err
	}
	if err := f.packPageInfo(); err != nil {
		return nil, err
	}
	return f.pageInfo, nil
}

关于prepareInfo方法,话题和回帖信息的获取都依赖topicid,这样2者就可以并行执行

func (f *QueryPageInfoFlow) prepareInfo() error {
	//获取topic信息
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
		f.topic = topic
	}()
	//获取post列表
	go func() {
		defer wg.Done()
		posts := repository.NewPostDaoInstance().QueryPostsByParentId(f.topicId)
		f.posts = posts
	}()
	wg.Wait()
	return nil
}

那么我们还剩下Paramcheck和pack函数,下面是完整代码(附上注释)

//query_page.go
// query_page.go
package service

import (//classproject是go.mod里面的模块
	"classproject/repository"
	"errors"
	"sync"
)

type PageInfo struct {
	Topic    *repository.Topic  // 主题信息
	PostList []*repository.Post // 帖子列表
}

// QueryPageInfo 根据主题 ID 查询并返回主题及帖子信息
func QueryPageInfo(topicId int64) (*PageInfo, error) {
	return NewQueryPageInfoFlow(topicId).Do()
}

// QueryPageInfo 根据主题 ID 查询并返回主题及帖子信息
func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
	return &QueryPageInfoFlow{
		topicId: topId,
	}
}

// QueryPageInfoFlow 结构定义了查询主题及帖子信息的流程
type QueryPageInfoFlow struct {
	topicId  int64
	pageInfo *PageInfo // 存储查询结果

	topic *repository.Topic
	posts []*repository.Post
}

// Do 执行查询主题及帖子信息的流程,返回查询结果或错误
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
	if err := f.checkParam(); err != nil {
		return nil, err
	}
	if err := f.prepareInfo(); err != nil {
		return nil, err
	}
	if err := f.packPageInfo(); err != nil {
		return nil, err
	}
	return f.pageInfo, nil
}

// checkParam 检查参数是否合法
func (f *QueryPageInfoFlow) checkParam() error {
	if f.topicId <= 0 {
		return errors.New("topic id must be larger than 0")
	}
	return nil
}

// prepareInfo 准备主题和帖子信息
func (f *QueryPageInfoFlow) prepareInfo() error {
	//获取topic信息
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
		f.topic = topic
	}()
	//获取post列表
	go func() {
		defer wg.Done()
		posts := repository.NewPostDaoInstance().QueryPostById(f.topicId)
		f.posts = posts
	}()
	wg.Wait()
	return nil
}

// packPageInfo 将主题和帖子列表信息打包成 PageInfo 结构
func (f *QueryPageInfoFlow) packPageInfo() error {
	f.pageInfo = &PageInfo{
		Topic:    f.topic,
		PostList: f.posts,
	}
	return nil
}

3.视图层

Service实现完成之后,下面就是controller(视图)层。这里我们定义一个view对象,通过code msg打包业务状态信息,用data承载业务实体信息,下面是代码。

//query_page.go
package cotroller

import (
	"strconv"

	"classproject/service"
)

// PageData 结构定义了返回给前端的数据格式
type PageData struct {
	Code int64       `json:"code"` // 返回状态码,0 表示成功,-1 表示错误
	Msg  string      `json:"msg"`  // 返回状态码,0 表示成功,-1 表示错误
	Data interface{} `json:"data"` // 返回状态码,0 表示成功,-1 表示错误
}

// QueryPageInfo 根据主题 ID 查询主题及帖子信息,并返回包装后的数据
func QueryPageInfo(topicIdStr string) *PageData {
	// 将传入的主题 ID 字符串转换为整型
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
	if err != nil { // 如果转换失败,返回包含错误信息的 PageData
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	// 调用 service 包中的 QueryPageInfo 函数查询主题及帖子信息
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		// 如果查询出错,返回包含错误信息的 PageData
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	// 查询成功,返回包含成功信息和查询结果的 PageData
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: pageInfo,
	}

}

三层都实现后,最后是web服务的引擎配置,包括初始化数据索引,初始化引擎配置,构建路由,启动服务。

//server.go
package main

import (
	"classproject/controller"
	"classproject/repository"
	"os"

	// "gopkg.in/gin-gonic/gin.v1"
	"github.com/gin-gonic/gin"
)

func main() {
	if err := Init("./data/"); err != nil {
		os.Exit(-1)
	} //初始化

	r := gin.Default() // 创建一个默认的 Gin 路由引擎
	// 定义一个 GET 请求处理函数
	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")                 // 获取请求中的主题 ID 参数
		data := cotroller.QueryPageInfo(topicId) // 调用控制器函数处理查询请求
		c.JSON(200, data)                        // 返回 JSON 格式的响应数据
	})
	err := r.Run() // 启动 Gin 服务器
	if err != nil {
		return
	}
}

// Init 函数用于初始化仓库数据
func Init(filePath string) error {
	if err := repository.Init(filePath); err != nil {
		return err
	}
	return nil
}

中间省略debug的无数错...一开始导包一直没弄好,然后后面发现代码又有很多小错误orz大家写的时候要仔细一点~

go run运行server.go之后,输入

curl http://localhost:8080/community/page/get/2

结果

image-20230810192127231.png

3.5课后实践

支持发布帖子。

本地ld生成需要保证不重复、唯一性。

Append 文件,更新索引,注意 Map 的并发安全问题。

//server.go
package main

import (
	"classproject/cotroller"
	"classproject/repository"
	"os"

	// "gopkg.in/gin-gonic/gin.v1"
	"github.com/gin-gonic/gin"
)

func main() {
	if err := Init("./data/"); err != nil {
		os.Exit(-1)
	} //初始化

	r := gin.Default() // 创建一个默认的 Gin 路由引擎
	// 定义一个 GET 请求处理函数
	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")                 // 获取请求中的主题 ID 参数
		data := cotroller.QueryPageInfo(topicId) // 调用控制器函数处理查询请求
		c.JSON(200, data)                        // 返回 JSON 格式的响应数据
	})
	r.POST("/community/post", func(c *gin.Context) {
		topicId := c.PostForm("topic_id")
		content := c.PostForm("content")
		data := cotroller.CreatePost(topicId, content)
		c.JSON(200, data)
	})

	err := r.Run() // 启动 Gin 服务器
	if err != nil {
		return
	}
}

// Init 函数用于初始化仓库数据
func Init(filePath string) error {
	if err := repository.Init(filePath); err != nil {
		return err
	}
	return nil
}

//init.go
package repository

import (
	"bufio"
	"encoding/json"
	"os"
)

// 使用索引数据结构提高查询速度
var (
	topicIndexMap map[int64]*Topic
	postIndexMap  map[int64][]*Post
)

// Init 函数用于初始化仓库,接收一个文件路径作为参数
func Init(filePath string) error {
	if err := initTopicIndexMap(filePath); err != nil {
		return err
	}
	if err := initPostIndexMap(filePath); err != nil {
		return err
	}
	return nil
}

// 初始化主题索引函数,接收一个文件路径作为参数
func initTopicIndexMap(filePath string) error {
	open, err := os.Open(filePath + "topic") //打开文件
	if err != nil {
		return err
	}
	defer open.Close() // 在函数结束时关闭文件

	// 创建一个扫描器来逐行读取文件内容
	scanner := bufio.NewScanner(open)
	topicTmpMap := make(map[int64]*Topic) // 创建一个临时map用于存储主题索引
	for scanner.Scan() {
		text := scanner.Text()
		var topic Topic
		// 将文本解析为 Topic 结构并存储到map中
		if err := json.Unmarshal([]byte(text), &topic); err != nil {
			return err
		}
		topicTmpMap[topic.Id] = &topic
	}
	topicIndexMap = topicTmpMap // 将临时map内容赋值给topic索引
	return nil
}

// post内容同理
func initPostIndexMap(filePath string) error {
	open, err := os.Open(filePath + "post")
	if err != nil {
		return err
	}

	defer open.Close()

	scanner := bufio.NewScanner(open)
	postTmpMap := make(map[int64][]*Post)
	for scanner.Scan() {
		text := scanner.Text()
		var post Post
		if err := json.Unmarshal([]byte(text), &post); err != nil {
			return err
		}
		posts, ok := postTmpMap[post.ParentId]
		if !ok {
			postTmpMap[post.ParentId] = []*Post{&post} //如果之前没有这个主题的帖子,就创建一个新的帖子切片,将当前的帖子放入其中,然后将这个切片与主题的父主题 ID 相关联。
			continue
		}
		posts = append(posts, &post) //将当前的帖子添加到之前已经存在的帖子分类中
		postTmpMap[post.ParentId] = posts
	}
	postIndexMap = postTmpMap
	return nil
}

//post.go
// post.go
package repository

import (
	"math/rand"
	"sync"
	"time"
)

var (
	idMutex  sync.Mutex
	usedIDs  = make(map[int64]bool)
	idSource *rand.Rand
)
var postMutex sync.Mutex

func init() {
	idSource = rand.New(rand.NewSource(time.Now().UnixNano()))
}

// 主题post的结构体定义
type Post struct {
	Id         int64  `json:"id"`
	ParentId   int64  `json:"parent_id"` //就是topic_id
	Content    string `json:"content"`
	CreateTime int64  `json:"create_time"`
}
type PostDao struct {
}

// GenerateUniqueID 用于生成唯一的帖子ID
func GenerateUniqueID() int64 {
	idMutex.Lock()
	defer idMutex.Unlock()

	var id int64
	for {
		id = int64(idSource.Uint64())
		if !usedIDs[id] {
			usedIDs[id] = true
			break
		}
	}
	return id
}

var (
	postDao  *PostDao
	postOnce sync.Once // 用于保证只初始化一次的同步机制
)

func NewPostDaoInstance() *PostDao {
	postOnce.Do(
		func() {
			postDao = &PostDao{}
		})
	return postDao
}

// CreatePost 创建帖子并更新索引
func (*PostDao) CreatePost(parentID int64, content string) error {
	postID := GenerateUniqueID() // 生成唯一的帖子 ID

	post := &Post{
		Id:         postID,
		ParentId:   parentID,
		Content:    content,
		CreateTime: time.Now().Unix(), // 设置帖子创建时间
	}
	postMutex.Lock()
	defer postMutex.Unlock()
	// 将帖子附加到帖子列表中
	postList, ok := postIndexMap[parentID]
	if !ok {
		postIndexMap[parentID] = []*Post{post}
	} else {
		postIndexMap[parentID] = append(postList, post)
	}

	return nil
}

// QueryPostById 根据主题 ID 查询对应的帖子信息
func (*PostDao) QueryPostById(parentId int64) []*Post {
	return postIndexMap[parentId] //// 返回与给定主题 ID 相关的帖子列表
}

serviced

// query_page.go
package service

import (
	"classproject/repository"
	"errors"
	"sync"
)

type PageInfo struct {
	Topic    *repository.Topic  // 主题信息
	PostList []*repository.Post // 帖子列表
}

// QueryPageInfo 根据主题 ID 查询并返回主题及帖子信息
func QueryPageInfo(topicId int64) (*PageInfo, error) {
	return NewQueryPageInfoFlow(topicId).Do()
}

// QueryPageInfo 根据主题 ID 查询并返回主题及帖子信息
func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
	return &QueryPageInfoFlow{
		topicId: topId,
	}
}

// QueryPageInfoFlow 结构定义了查询主题及帖子信息的流程
type QueryPageInfoFlow struct {
	topicId  int64
	pageInfo *PageInfo // 存储查询结果

	topic *repository.Topic
	posts []*repository.Post
}

// Do 执行查询主题及帖子信息的流程,返回查询结果或错误
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
	if err := f.checkParam(); err != nil {
		return nil, err
	}
	if err := f.prepareInfo(); err != nil {
		return nil, err
	}
	if err := f.packPageInfo(); err != nil {
		return nil, err
	}
	return f.pageInfo, nil
}

// checkParam 检查参数是否合法
func (f *QueryPageInfoFlow) checkParam() error {
	if f.topicId <= 0 {
		return errors.New("topic id must be larger than 0")
	}
	return nil
}

// prepareInfo 准备主题和帖子信息
func (f *QueryPageInfoFlow) prepareInfo() error {
	//获取topic信息
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
		f.topic = topic
	}()
	//获取post列表
	go func() {
		defer wg.Done()
		posts := repository.NewPostDaoInstance().QueryPostById(f.topicId)
		f.posts = posts
	}()
	wg.Wait()
	return nil
}

// packPageInfo 将主题和帖子列表信息打包成 PageInfo 结构
func (f *QueryPageInfoFlow) packPageInfo() error {
	f.pageInfo = &PageInfo{
		Topic:    f.topic,
		PostList: f.posts,
	}
	return nil
}

// CreatePost 创建帖子并更新索引
func (f *QueryPageInfoFlow) CreatePost(content string) error {
	return repository.NewPostDaoInstance().CreatePost(f.topicId, content)
}

controller的

// query_page.go
package cotroller

import (
	"classproject/service"
	"strconv"
)

// PageData 结构定义了返回给前端的数据格式
type PageData struct {
	Code int64       `json:"code"` // 返回状态码,0 表示成功,-1 表示错误
	Msg  string      `json:"msg"`  // 返回状态码,0 表示成功,-1 表示错误
	Data interface{} `json:"data"` // 返回状态码,0 表示成功,-1 表示错误
}

// QueryPageInfo 根据主题 ID 查询主题及帖子信息,并返回包装后的数据
func QueryPageInfo(topicIdStr string) *PageData {
	// 将传入的主题 ID 字符串转换为整型
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
	if err != nil { // 如果转换失败,返回包含错误信息的 PageData
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	// 调用 service 包中的 QueryPageInfo 函数查询主题及帖子信息
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		// 如果查询出错,返回包含错误信息的 PageData
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	// 查询成功,返回包含成功信息和查询结果的 PageData
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: pageInfo,
	}

}

func CreatePost(topicIdStr string, content string) *PageData {
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}

	// 调用创建帖子方法
	err = service.NewQueryPageInfoFlow(topicId).CreatePost(content)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}

	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: nil,
	}
}

Invoke-WebRequest -Method Post -Uri "http://localhost:8080/community/post" -Body "topic_id=10&content=zlx%20nb!!!"

image-20230810200927150.png curl http://localhost:8080/community/page/get/10

image-20230810202443674.png