Go 语言工程实践| 青训营笔记

43 阅读16分钟
package attention

import "fmt"

func AppendInt() {
	intArray := [3]int64{1, 2, 3}
	func(arr [3]int64) {
		arr[2] = 4
		fmt.Println("inner func array:",arr)
	}(intArray)

	fmt.Println("outer func array:",intArray)
}

这段代码定义了一个名为 AppendInt 的函数,其目的是演示对数组的操作和传递。

首先,在 package attention 中导入了 "fmt" 包,以便在代码中使用 fmt.Println 函数。

然后,定义了 AppendInt 函数,该函数没有参数和返回值。

在函数体内部,声明了一个长度为 3 的整型数组 intArray,并初始化为 [1, 2, 3]

接下来,定义了一个匿名函数,并立即调用该函数。这个匿名函数接受一个数组作为参数,并在函数体内部对数组进行操作。

在匿名函数中,将传入的数组的索引为 2 的元素修改为 4,然后使用 fmt.Println 函数打印修改后的数组。

在匿名函数执行完毕后,控制流回到 AppendInt 函数,使用 fmt.Println 函数打印未被修改的 intArray 数组。

最终的输出结果是:

inner func array: [1 2 4]
outer func array: [1 2 3]

可以看到,在匿名函数中修改的数组只在匿名函数内部有效,不会影响到外部的 intArray 数组。这是因为在 Go 语言中,数组作为参数传递给函数时是按值传递的,即传递的是数组的副本而不是原始数组本身。因此,在匿名函数中对传入的数组进行修改只会影响到副本,而不会修改原始数组。

package attention

import "testing"

func TestAppendInt(t *testing.T) {
	AppendInt()
}

这段代码是一个测试代码,用于测试之前提到的 AppendInt 函数。

首先,在 package attention 中导入了 "testing" 包,以便在代码中使用测试相关的函数和结构。

然后,定义了一个名为 TestAppendInt 的测试函数,它接受一个 *testing.T 类型的参数 t

在函数体内部,调用了之前提到的 AppendInt 函数。

测试函数的目的是确保 AppendInt 函数能够正常运行而不发生任何错误。如果 AppendInt 函数内部存在错误,测试函数会在运行过程中引发错误,并将错误信息记录在 *testing.T 类型的参数 t 中。

通过这样的测试函数,可以在执行测试时查看输出结果和错误信息,并确保代码的正确性。

要运行这个测试函数,你可以使用测试框架(如 Go 自带的 go test 命令)来运行测试。

package attention

import (
	"time"
)

func closure() {
	for i := 0; i < 3; i++ {
		go func() {
			println(i)
		}()
	}
	time.Sleep(3 * time.Second)
}

func closure1() {
	for i := 0; i < 3; i++ {
		go func(j int) {
			println(j)
		}(i)
	}
	time.Sleep(3 * time.Second)
}

这段代码展示了两个函数 closureclosure1,它们演示了闭包在并发编程中的一些特性。

首先,在代码中导入了 "time" 包,以便在程序中使用时间相关的功能。

closure 函数定义了一个循环,从 0 到 2 迭代一个变量 i。在每次迭代中,它创建了一个匿名函数并立即启动一个新的 goroutine 来执行该函数。匿名函数打印变量 i 的值。由于匿名函数是在 goroutine 中执行的,它们可以并发地执行。

然而,由于闭包中的变量 i 是通过引用捕获的,而不是通过值传递的,因此在 goroutine 执行时,循环可能已经继续执行,导致 i 的值已经被修改。因此,所有的 goroutine 最终都会打印出 3,而不是预期的 0、1、2。

为了解决这个问题,closure1 函数使用了一个不同的方法。在每次迭代中,它创建了一个匿名函数,并将循环变量 i 作为参数传递给匿名函数。通过将 i 的值传递给匿名函数的参数 j,确保每个 goroutine 在执行时使用的是不同的变量值。这样,每个 goroutine 打印的值就是预期的 0、1、2。

最后,通过调用 time.Sleep(3 * time.Second) 函数,主函数会等待 3 秒钟,以确保所有 goroutine 完成执行。这样可以确保在程序退出之前所有的 goroutine 都有足够的时间打印输出。

package attention

import (
	"bytes"
	"encoding/json"
	"fmt"
)

func NumUnmarshal(){
	jsonStr:=`{"id":1,"name":"Jerry"}`
	var res map[string]interface{}
	_ = json.Unmarshal([]byte(jsonStr), &res)
	fmt.Printf("%T\n",res["id"])
	i := res["id"].(int64)
	fmt.Println(i)

}

func NumDecode(){
	jsonStr:=`{"id":1,"name":"Jerry"}`
	var res map[string]interface{}
	decoder := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
	decoder.UseNumber()
	_ = decoder.Decode(&res)
	i,_ := res["id"].(json.Number).Int64()
	fmt.Println(i)

}

以下是对代码的逐行解释:

import (
	"bytes"
	"encoding/json"
	"fmt"
)

这段代码导入了三个包:bytesencoding/jsonfmtbytes 包提供了操作字节切片的函数,encoding/json 包用于 JSON 的编码和解码,fmt 包用于格式化和打印输出。

func NumUnmarshal(){
	jsonStr:=`{"id":1,"name":"Jerry"}`
	var res map[string]interface{}
	_ = json.Unmarshal([]byte(jsonStr), &res)
	fmt.Printf("%T\n",res["id"])
	i := res["id"].(int64)
	fmt.Println(i)
}

这段代码定义了一个名为 NumUnmarshal 的函数。在函数内部,定义了一个 JSON 字符串 jsonStr,表示一个包含 idname 字段的 JSON 对象。

接下来,声明了一个变量 res,它是一个空的 map[string]interface{} 类型,用于存储 JSON 解码后的结果。

然后,使用 json.Unmarshal 函数将 JSON 字符串解码为 Go 的数据结构,并将解码结果存储到 res 变量中。

接着,使用 fmt.Printf 打印出 res["id"] 的类型。%T 是一个格式化占位符,用于输出值的类型信息。

然后,将 res["id"] 断言为 int64 类型,并将结果存储到变量 i 中。

最后,使用 fmt.Println 打印出变量 i 的值。

func NumDecode(){
	jsonStr:=`{"id":1,"name":"Jerry"}`
	var res map[string]interface{}
	decoder := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
	decoder.UseNumber()
	_ = decoder.Decode(&res)
	i,_ := res["id"].(json.Number).Int64()
	fmt.Println(i)
}

这段代码定义了一个名为 NumDecode 的函数,它与 NumUnmarshal 函数的目的相同,即将 JSON 字符串解码为 Go 的数据结构。

在函数内部,首先定义了一个 JSON 字符串 jsonStr 和一个空的 map[string]interface{} 类型的变量 res,与之前的函数相似。

然后,使用 json.NewDecoder 函数创建一个 JSON 解码器,并将 JSON 字符串的字节切片传递给解码器。

接着,通过调用 decoder.UseNumber(),设置解码器的选项,以便在解码过程中将数字解码为 json.Number 类型。

然后,使用 decoder.Decode 函数将 JSON 解码为 Go 的数据结构,并将结果存储到 res 变量中。

接下来,通过断言将 res["id"] 转换为 json.Number 类型,并使用 Int64() 方法将其转换为 int64 类型,并将结果存储到变量 i 中。

最后,使用 fmt.Println 打印出变量 i 的值。

package attention

import (
	"unicode/utf8"
)

func length(){
	str:="⬇汉字"
	println(len(str))
}


func length1(){
	str:="⬇汉字"
	println(utf8.RuneCountInString(str))
}

以下是对代码的逐行解释:

import (
	"unicode/utf8"
)

这段代码导入了 "unicode/utf8" 包,用于处理 Unicode 字符串的相关函数。

func length(){
	str:="⬇汉字"
	println(len(str))
}

这段代码定义了一个名为 length 的函数。在函数内部,定义了一个字符串变量 str,其值为 "⬇汉字",包含了一个箭头符号和两个汉字。

然后,使用 len() 函数打印出字符串 str 的字节长度。由于 Go 语言中的字符串是基于字节的,len() 函数返回的是字符串的字节长度。

func length1(){
	str:="⬇汉字"
	println(utf8.RuneCountInString(str))
}

这段代码定义了一个名为 length1 的函数,与之前的函数目的相同。

在函数内部,同样定义了一个字符串变量 str,其值为 "⬇汉字"

然后,使用 utf8.RuneCountInString() 函数计算字符串 str 中 Unicode 字符的数量,并打印出结果。

由于 Unicode 字符可能由多个字节表示,因此直接使用 len() 函数计算的是字符串的字节长度,而不是字符长度。使用 utf8.RuneCountInString() 函数可以正确计算字符串中 Unicode 字符的数量。

通过这两个函数,我们可以得到字符串的字节长度和字符长度,从而更准确地处理包含 Unicode 字符的字符串。

package concurrence

func CalSquare() {
	src := make(chan int)
	dest := make(chan int, 3)
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()
	for i := range dest {
		println(i)
	}
}

以下是对代码的逐行解释:

func CalSquare() {

这段代码定义了一个名为 CalSquare 的函数,用于计算数字的平方。

src := make(chan int)
dest := make(chan int, 3)

这两行代码声明了两个通道变量 srcdestsrc 是一个无缓冲通道,用于将数字发送给计算平方的 goroutine。dest 是一个带有缓冲区大小为 3 的通道,用于接收计算得到的平方值。

go func() {
	defer close(src)
	for i := 0; i < 10; i++ {
		src <- i
	}
}()

这段代码启动一个匿名的 goroutine,用于向 src 通道发送数字。通过循环,将数字 0 到 9 发送到 src 通道中。使用 defer 关键字在 goroutine 结束时关闭 src 通道。

go func() {
	defer close(dest)
	for i := range src {
		dest <- i * i
	}
}()

这段代码启动另一个匿名的 goroutine,用于从 src 通道接收数字并计算平方后发送到 dest 通道。通过 range src 循环,不断从 src 通道接收数字,并将其平方值发送到 dest 通道中。使用 defer 关键字在 goroutine 结束时关闭 dest 通道。

for i := range dest {
	println(i)
}

这段代码使用 range dest 循环从 dest 通道接收计算得到的平方值,并将其打印出来。循环将一直进行,直到 dest 通道关闭,即所有平方值都被接收并处理。

整个流程可以总结为以下几个步骤:

  1. 启动一个 goroutine,向 src 通道发送数字。
  2. 启动另一个 goroutine,从 src 通道接收数字并计算平方后发送到 dest 通道。
  3. 在主 goroutine 中,循环从 dest 通道接收计算得到的平方值并打印出来。
  4. 当所有平方值都被接收后,dest 通道关闭,循环结束。
package concurrence

import (
	"fmt"
	"sync"
)

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

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

以下是对代码的逐行解释:

import (
	"fmt"
	"sync"
)

这段代码导入了 "fmt""sync" 包。"fmt" 包提供了格式化和打印输出的函数,"sync" 包提供了同步操作的函数和类型。

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

这段代码定义了一个名为 hello 的函数,用于打印 "hello world" 加上一个整数参数的字符串。

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

这段代码定义了一个名为 ManyGo 的函数。在函数内部,首先声明了一个 sync.WaitGroup 类型的变量 wg,用于等待并同步多个 goroutine。

然后,使用 for 循环创建了 5 个 goroutine。在每次循环中,调用 wg.Add(1)wg 的计数器加一,表示要等待一个额外的 goroutine。

接着,启动了一个匿名的 goroutine。这个 goroutine 接受一个参数 j,并在结束时调用 wg.Done() 来通知 wg 计数器减一。

在 goroutine 的函数体内,调用了 hello(j) 函数,将参数 j 传递给 hello 函数。

最后,通过调用 wg.Wait() 阻塞主 goroutine,直到所有的 goroutine 都执行完毕。这样可以确保在 wg 计数器变为零之前,主 goroutine 不会继续执行。

整个流程可以总结为以下几个步骤:

  1. 声明一个 sync.WaitGroup 类型的变量 wg,用于等待和同步多个 goroutine。
  2. 使用循环创建多个 goroutine。
  3. 每次循环中,调用 wg.Add(1)wg 的计数器加一,表示要等待一个额外的 goroutine。
  4. 启动一个匿名的 goroutine,接受一个参数 j,在结束时调用 wg.Done()
  5. 在 goroutine 的函数体内,调用其他函数并传递参数。
  6. 调用 wg.Wait() 阻塞主 goroutine,直到所有的 goroutine 执行完毕。
package handler

import (
	"strconv"

	"github.com/Moonlight-Zhao/go-project-example/service"
)

type PageData struct {
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}

func QueryPageInfo(topicIdStr string) *PageData {
	//参数转换
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	//获取service层结果
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: pageInfo,
	}

}

以下是对代码的逐行解释:

import (
	"strconv"

	"github.com/Moonlight-Zhao/go-project-example/service"
)

这段代码导入了 "strconv" 包和一个自定义的包 "github.com/Moonlight-Zhao/go-project-example/service""strconv" 包提供了字符串和基本类型之间的转换函数。

type PageData struct {
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}

这段代码定义了一个结构体类型 PageData,用于表示页面数据。它包含了三个字段:CodeMsgData。这些字段的类型分别为 int64stringinterface{}。结构体的字段标签 json:"code"json:"msg"json:"data" 指定了在 JSON 序列化和反序列化时对应的字段名称。

func QueryPageInfo(topicIdStr string) *PageData {
	//参数转换
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	//获取service层结果
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: pageInfo,
	}
}

这段代码定义了一个名为 QueryPageInfo 的函数,用于查询页面信息。

函数接受一个 topicIdStr 参数,表示主题ID的字符串形式。首先,通过调用 strconv.ParseInt 函数将 topicIdStr 转换为 int64 类型的 topicId。如果转换过程中发生错误,函数会返回一个带有错误信息的 PageData 结构体指针,其中 Code 被设置为 -1,Msg 被设置为错误的字符串描述。

接着,调用 service.QueryPageInfo 函数获取服务层的结果 pageInfo。如果获取结果过程中发生错误,函数会返回一个带有错误信息的 PageData 结构体指针,同样设置 Code 为 -1,Msg 为错误的字符串描述。

最后,如果查询和获取结果都成功,函数会返回一个 PageData 结构体指针,其中 Code 被设置为 0,Msg 被设置为 "success",Data 被设置为 pageInfo

通过这个函数,可以将主题ID字符串转换为整数并调用服务层获取页面信息,然后根据查询和获取结果的情况返回相应的 PageData 结构体指针。

package handler

import (
	"github.com/Moonlight-Zhao/go-project-example/service"
	"strconv"
)

func PublishPost(uidStr, topicIdStr, content string) *PageData {
	//参数转换
	uid, _ := strconv.ParseInt(uidStr, 10, 64)
	topic, _ := strconv.ParseInt(topicIdStr, 10, 64)
	//获取service层结果
	postId, err := service.PublishPost(topic, uid, content)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: map[string]int64{
			"post_id": postId,
		},
	}

}

以下是对代码的逐行解释:

import (
	"github.com/Moonlight-Zhao/go-project-example/service"
	"strconv"
)

这段代码导入了 "github.com/Moonlight-Zhao/go-project-example/service" 包和 "strconv" 包。前者是一个自定义的包,后者提供了字符串和基本类型之间的转换函数。

func PublishPost(uidStr, topicIdStr, content string) *PageData {
	//参数转换
	uid, _ := strconv.ParseInt(uidStr, 10, 64)
	topic, _ := strconv.ParseInt(topicIdStr, 10, 64)
	//获取service层结果
	postId, err := service.PublishPost(topic, uid, content)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: map[string]int64{
			"post_id": postId,
		},
	}
}

这段代码定义了一个名为 PublishPost 的函数,用于发布帖子。

函数接受三个参数:uidStrtopicIdStrcontent,分别表示用户ID的字符串形式、主题ID的字符串形式和帖子内容。

首先,通过调用 strconv.ParseInt 函数将 uidStrtopicIdStr 转换为 int64 类型的变量 uidtopic。由于这里使用了 _ 来忽略返回的错误值,所以在转换过程中的错误将被忽略。

接着,调用 service.PublishPost 函数来发布帖子,并将结果赋值给变量 postId。如果发布过程中发生错误,函数会返回一个带有错误信息的 PageData 结构体指针,其中 Code 被设置为 -1,Msg 被设置为错误的字符串描述。

最后,如果发布成功,函数会返回一个 PageData 结构体指针,其中 Code 被设置为 0,Msg 被设置为 "success",Data 被设置为一个包含帖子ID的 map[string]int64 类型。

通过这个函数,可以将用户ID和主题ID字符串转换为整数,并调用服务层发布帖子。根据发布结果的情况返回相应的 PageData 结构体指针。

package repository

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var db *gorm.DB

func Init() error {
	var err error
	dsn := "root:00000000@tcp(127.0.0.1:3306)/community?charset=utf8mb4&parseTime=True&loc=Local"
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	return err
}

以下是对代码的逐行解释:

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

这段代码导入了 "gorm.io/driver/mysql""gorm.io/gorm" 包。"gorm.io/driver/mysql" 包提供了与 MySQL 数据库的连接和操作相关的功能,"gorm.io/gorm" 包是 GORM ORM 库的主要包,用于进行数据库操作。

var db *gorm.DB

这段代码声明了一个全局变量 db,其类型为 *gorm.DBgorm.DB 是 GORM 库提供的数据库连接对象。

func Init() error {
	var err error
	dsn := "root:00000000@tcp(127.0.0.1:3306)/community?charset=utf8mb4&parseTime=True&loc=Local"
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	return err
}

这段代码定义了一个名为 Init 的函数,用于初始化数据库连接。

函数内部首先声明了一个 err 变量,用于存储可能发生的错误。

然后,定义了一个 dsn 变量,其中包含了连接数据库所需的数据源名称。这个数据源名称指定了数据库的地址、用户名、密码、数据库名以及其他连接选项,比如字符集和解析时间等。

接着,调用 gorm.Open 函数来打开数据库连接。这个函数接受两个参数,第一个参数是数据库驱动名,这里使用的是 "mysql" 驱动;第二个参数是一个 gorm.Config 类型的指针,用于配置数据库连接的一些选项。

gorm.Open 函数返回一个 *gorm.DB 类型的数据库连接对象,将其赋值给全局变量 db

最后,将可能发生的错误返回给调用者。

通过调用这个 Init 函数,可以初始化全局的数据库连接 db,使其可以用于后续的数据库操作。

package repository

import (
	"github.com/Moonlight-Zhao/go-project-example/util"
	"gorm.io/gorm"
	"sync"
	"time"
)

type Post struct {
	Id         int64     `gorm:"column:id"`
	ParentId   int64     `gorm:"parent_id"`
	UserId     int64     `gorm:"column:user_id"`
	Content    string    `gorm:"column:content"`
	DiggCount  int32     `gorm:"column:digg_count"`
	CreateTime time.Time `gorm:"column:create_time"`
}

func (Post) TableName() string {
	return "post"
}

type PostDao struct {
}

var postDao *PostDao
var postOnce sync.Once

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

func (*PostDao) QueryPostById(id int64) (*Post, error) {
	var post Post
	err := db.Where("id = ?", id).Find(&post).Error
	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		util.Logger.Error("find post by id err:" + err.Error())
		return nil, err
	}
	return &post, nil
}

func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) {
	var posts []*Post
	err := db.Where("parent_id = ?", parentId).Find(&posts).Error
	if err != nil {
		util.Logger.Error("find posts by parent_id err:" + err.Error())
		return nil, err
	}
	return posts, nil
}

func (*PostDao) CreatePost(post *Post) error {
	if err := db.Create(post).Error; err != nil {
		util.Logger.Error("insert post err:" + err.Error())
		return err
	}
	return nil
}

以下是对代码的逐行解释:

import (
	"github.com/Moonlight-Zhao/go-project-example/util"
	"gorm.io/gorm"
	"sync"
	"time"
)

这段代码导入了 "github.com/Moonlight-Zhao/go-project-example/util" 包,"gorm.io/gorm" 包,"sync" 包和 "time" 包。前者是一个自定义的包,后者提供了与时间相关的功能,"sync" 包提供了并发相关的功能。

type Post struct {
	Id         int64     `gorm:"column:id"`
	ParentId   int64     `gorm:"parent_id"`
	UserId     int64     `gorm:"column:user_id"`
	Content    string    `gorm:"column:content"`
	DiggCount  int32     `gorm:"column:digg_count"`
	CreateTime time.Time `gorm:"column:create_time"`
}

这段代码定义了一个名为 Post 的结构体。该结构体表示一个帖子,包含了帖子的各个属性。使用了 gormcolumn tag 来指定结构体字段与数据库表列之间的映射关系。

func (Post) TableName() string {
	return "post"
}

这是 Post 结构体的一个方法,用于返回数据库表名。在这里,它返回了字符串 "post",表示该结构体对应的数据库表名为 "post"

type PostDao struct {
}

这段代码定义了一个名为 PostDao 的结构体。该结构体表示帖子的数据访问对象(DAO),用于封装对帖子数据的访问操作。

var postDao *PostDao
var postOnce sync.Once

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

这段代码定义了一个名为 NewPostDaoInstance 的函数,用于获取 PostDao 的单例实例。

在函数内部,使用了 sync.Once 类型的 postOnce 变量来保证只执行一次实例化操作。在第一次调用函数时,使用匿名函数初始化 postDao 变量,创建一个 PostDao 结构体的实例,并将其赋值给 postDao 变量。

最后,返回 postDao 变量作为 PostDao 的单例实例。

func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) {
	var posts []*Post
	err := db.Where("parent_id = ?", parentId).Find(&posts).Error
	if err != nil {
		util.Logger.Error("find posts by parent_id err:" + err.Error())
		return nil, err
	}
	return posts, nil
}

这是 PostDao 结构体的另一个方法,用于根据父帖子ID查询相关的子帖子信息。

方法接受一个 parentId 参数,表示要查询的父帖子ID。在方法内部,使用 db 对象执行数据库查询操作,根据 parentId 查询相关的子帖子信息,并将结果存储在 posts 切片中。

如果查询过程中发生错误,会将错误信息记录到日志并返回错误。否则,返回查询到的子帖子切片。

func (*PostDao) CreatePost(post *Post) error {
	if err := db.Create(post).Error; err != nil {
		util.Logger.Error("insert post err:" + err.Error())
		return err
	}
	return nil
}

这是 PostDao 结构体的另一个方法,用于创建新的帖子。

方法接受一个 post 参数,表示要创建的帖子对象。在方法内部,使用 db 对象执行数据库插入操作,将 post 对象插入数据库中。

如果插入过程中发生错误,会将错误信息记录到日志并返回错误。否则,返回 nil 表示插入成功。

通过这些方法,PostDao 提供了对帖子数据的查询和创建操作,通过调用这些方法可以与数据库交互,对帖子数据进行存取操作。

package repository

import (
	"github.com/Moonlight-Zhao/go-project-example/util"
	"sync"
	"time"
)

type Topic struct {
	Id         int64     `gorm:"column:id"`
	UserId     int64     `gorm:"column:user_id"`
	Title      string    `gorm:"column:title"`
	Content    string    `gorm:"column:content"`
	CreateTime time.Time `gorm:"column:create_time"`
}

func (Topic) TableName() string {
	return "topic"
}

type TopicDao struct {
}

var topicDao *TopicDao
var topicOnce sync.Once

func NewTopicDaoInstance() *TopicDao {
	topicOnce.Do(
		func() {
			topicDao = &TopicDao{}
		})
	return topicDao
}

func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {
	var topic Topic
	err := db.Where("id = ?", id).Find(&topic).Error
	if err != nil {
		util.Logger.Error("find topic by id err:" + err.Error())
		return nil, err
	}
	return &topic, nil
}

以下是对代码的逐行解释:

import (
	"github.com/Moonlight-Zhao/go-project-example/util"
	"sync"
	"time"
)

这段代码导入了 "github.com/Moonlight-Zhao/go-project-example/util" 包,"sync" 包和 "time" 包。前者是一个自定义的包,后者提供了与时间相关的功能,"sync" 包提供了并发相关的功能。

type Topic struct {
	Id         int64     `gorm:"column:id"`
	UserId     int64     `gorm:"column:user_id"`
	Title      string    `gorm:"column:title"`
	Content    string    `gorm:"column:content"`
	CreateTime time.Time `gorm:"column:create_time"`
}

这段代码定义了一个名为 Topic 的结构体。该结构体表示一个话题,包含了话题的各个属性。使用了 gormcolumn tag 来指定结构体字段与数据库表列之间的映射关系。

func (Topic) TableName() string {
	return "topic"
}

这是 Topic 结构体的一个方法,用于返回数据库表名。在这里,它返回了字符串 "topic",表示该结构体对应的数据库表名为 "topic"

type TopicDao struct {
}

这段代码定义了一个名为 TopicDao 的结构体。该结构体表示话题的数据访问对象(DAO),用于封装对话题数据的访问操作。

var topicDao *TopicDao
var topicOnce sync.Once

func NewTopicDaoInstance() *TopicDao {
	topicOnce.Do(
		func() {
			topicDao = &TopicDao{}
		})
	return topicDao
}

这段代码定义了一个名为 NewTopicDaoInstance 的函数,用于获取 TopicDao 的单例实例。

在函数内部,使用了 sync.Once 类型的 topicOnce 变量来保证只执行一次实例化操作。在第一次调用函数时,使用匿名函数初始化 topicDao 变量,创建一个 TopicDao 结构体的实例,并将其赋值给 topicDao 变量。

最后,返回 topicDao 变量作为 TopicDao 的单例实例。

func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {
	var topic Topic
	err := db.Where("id = ?", id).Find(&topic).Error
	if err != nil {
		util.Logger.Error("find topic by id err:" + err.Error())
		return nil, err
	}
	return &topic, nil
}

这是 TopicDao 结构体的一个方法,用于根据话题ID查询话题信息。

方法接受一个 id 参数,表示要查询的话题ID。在方法内部,使用 db 对象执行数据库查询操作,根据 id 查询对应的话题信息,并将结果存储在 topic 变量中。

如果查询过程中发生错误,会将错误信息记录到日志并返回错误。否则,返回查询到的话题对象的指针。

package repository

import (
	"github.com/Moonlight-Zhao/go-project-example/util"
	"gorm.io/gorm"
	"sync"
	"time"
)

type User struct {
	Id         int64     `gorm:"column:id"`
	Name       string    `gorm:"column:name"`
	Avatar     string    `gorm:"column:avatar"`
	Level      int       `gorm:"column:level"`
	CreateTime time.Time `gorm:"column:create_time"`
	ModifyTime time.Time `gorm:"column:modify_time"`
}

func (User) TableName() string {
	return "user"
}

type UserDao struct {
}

var userDao *UserDao
var userOnce sync.Once

func NewUserDaoInstance() *UserDao {
	userOnce.Do(
		func() {
			userDao = &UserDao{}
		})
	return userDao
}

func (*UserDao) QueryUserById(id int64) (*User, error) {
	var user User
	err := db.Where("id = ?", id).Find(&user).Error
	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		util.Logger.Error("find user by id err:" + err.Error())
		return nil, err
	}
	return &user, nil
}

func (*UserDao) MQueryUserById(ids []int64) (map[int64]*User, error) {
	var users []*User
	err := db.Where("id in (?)", ids).Find(&users).Error
	if err != nil {
		util.Logger.Error("batch find user by id err:" + err.Error())
		return nil, err
	}
	userMap := make(map[int64]*User)
	for _, user := range users {
		userMap[user.Id] = user
	}
	return userMap, nil
}

以下是对代码的逐行解释:

import (
	"github.com/Moonlight-Zhao/go-project-example/util"
	"gorm.io/gorm"
	"sync"
	"time"
)

这段代码导入了 "github.com/Moonlight-Zhao/go-project-example/util" 包,"gorm.io/gorm" 包,"sync" 包和 "time" 包。前者是一个自定义的包,后者提供了与时间相关的功能,"gorm.io/gorm" 包是 GORM ORM 库,"sync" 包提供了并发相关的功能。

type User struct {
	Id         int64     `gorm:"column:id"`
	Name       string    `gorm:"column:name"`
	Avatar     string    `gorm:"column:avatar"`
	Level      int       `gorm:"column:level"`
	CreateTime time.Time `gorm:"column:create_time"`
	ModifyTime time.Time `gorm:"column:modify_time"`
}

这段代码定义了一个名为 User 的结构体。该结构体表示一个用户,包含了用户的各个属性。使用了 gormcolumn tag 来指定结构体字段与数据库表列之间的映射关系。

func (User) TableName() string {
	return "user"
}

这是 User 结构体的一个方法,用于返回数据库表名。在这里,它返回了字符串 "user",表示该结构体对应的数据库表名为 "user"

type UserDao struct {
}

这段代码定义了一个名为 UserDao 的结构体。该结构体表示用户的数据访问对象(DAO),用于封装对用户数据的访问操作。

var userDao *UserDao
var userOnce sync.Once

func NewUserDaoInstance() *UserDao {
	userOnce.Do(
		func() {
			userDao = &UserDao{}
		})
	return userDao
}

这段代码定义了一个名为 NewUserDaoInstance 的函数,用于获取 UserDao 的单例实例。

在函数内部,使用了 sync.Once 类型的 userOnce 变量来保证只执行一次实例化操作。在第一次调用函数时,使用匿名函数初始化 userDao 变量,创建一个 UserDao 结构体的实例,并将其赋值给 userDao 变量。

最后,返回 userDao 变量作为 UserDao 的单例实例。

func (*UserDao) QueryUserById(id int64) (*User, error) {
	var user User
	err := db.Where("id = ?", id).Find(&user).Error
	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		util.Logger.Error("find user by id err:" + err.Error())
		return nil, err
	}
	return &user, nil
}

这是 UserDao 结构体的一个方法,用于根据用户ID查询用户信息。

方法接受一个 id 参数,表示要查询的用户ID。在方法内部,使用 db 对象执行数据库查询操作,根据 id 查询对应的用户信息,并将结果存储在user 变量中。

如果查询结果为空(即用户不存在),则返回 nil, nil,表示未找到用户。

如果查询过程中发生错误,会将错误信息记录到日志中,并返回 nil, err,表示查询出错。

如果查询成功,将查询到的用户信息返回给调用方。

func (*UserDao) MQueryUserById(ids []int64) (map[int64]*User, error) {
	var users []*User
	err := db.Where("id in (?)", ids).Find(&users).Error
	if err != nil {
		util.Logger.Error("batch find user by id err:" + err.Error())
		return nil, err
	}
	userMap := make(map[int64]*User)
	for _, user := range users {
		userMap[user.Id] = user
	}
	return userMap, nil
}

这是 UserDao 结构体的另一个方法,用于批量根据用户ID查询用户信息。

方法接受一个 ids 参数,表示要查询的用户ID列表。在方法内部,使用 db 对象执行数据库查询操作,根据 ids 查询对应的用户信息,并将结果存储在 users 切片中。

如果查询过程中发生错误,会将错误信息记录到日志中,并返回 nil, err,表示查询出错。

如果查询成功,将查询到的用户信息转换为以用户ID为键、用户信息为值的 map,并返回给调用方。

这样,调用方可以通过用户ID快速访问对应的用户信息。

package service

import (
	"errors"
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"time"
	"unicode/utf8"
)

func PublishPost(topicId, userId int64, content string) (int64, error) {
	return NewPublishPostFlow(topicId, userId, content).Do()
}

func NewPublishPostFlow(topicId, userId int64, content string) *PublishPostFlow {
	return &PublishPostFlow{
		userId:  userId,
		content: content,
		topicId: topicId,
	}
}

type PublishPostFlow struct {
	userId  int64
	content string
	topicId int64

	postId int64
}

func (f *PublishPostFlow) Do() (int64, error) {
	if err := f.checkParam(); err != nil {
		return 0, err
	}
	if err := f.publish(); err != nil {
		return 0, err
	}
	return f.postId, nil
}

func (f *PublishPostFlow) checkParam() error {
	if f.userId <= 0 {
		return errors.New("userId id must be larger than 0")
	}
	if utf8.RuneCountInString(f.content) >= 500 {
		return errors.New("content length must be less than 500")
	}
	return nil
}

func (f *PublishPostFlow) publish() error {
	post := &repository.Post{
		ParentId:   f.topicId,
		UserId:     f.userId,
		Content:    f.content,
		CreateTime: time.Now(),
	}
	if err := repository.NewPostDaoInstance().CreatePost(post); err != nil {
		return err
	}
	f.postId = post.Id
	return nil
}

这段代码位于 service 包中,主要是用于实现发布帖子的功能。

func PublishPost(topicId, userId int64, content string) (int64, error) {
	return NewPublishPostFlow(topicId, userId, content).Do()
}

这是 service 包中的一个公共函数,用于发布帖子。函数接受 topicIduserIdcontent 作为参数,表示帖子的主题ID、发布用户的ID以及帖子的内容。函数通过创建一个 PublishPostFlow 对象,并调用其 Do 方法来执行发布帖子的流程。

func NewPublishPostFlow(topicId, userId int64, content string) *PublishPostFlow {
	return &PublishPostFlow{
		userId:  userId,
		content: content,
		topicId: topicId,
	}
}

NewPublishPostFlow 函数用于创建一个 PublishPostFlow 对象,并初始化其属性。函数接受 topicIduserIdcontent 作为参数,将这些参数作为 PublishPostFlow 对象的属性,并返回该对象的指针。

type PublishPostFlow struct {
	userId  int64
	content string
	topicId int64

	postId int64
}

PublishPostFlow 结构体表示发布帖子的流程。它包含了 userIdcontenttopicIdpostId 四个属性。

func (f *PublishPostFlow) Do() (int64, error) {
	if err := f.checkParam(); err != nil {
		return 0, err
	}
	if err := f.publish(); err != nil {
		return 0, err
	}
	return f.postId, nil
}

PublishPostFlow 结构体定义了一个 Do 方法,用于执行发布帖子的流程。在 Do 方法中,首先调用 checkParam 方法来检查参数的合法性,如果参数校验失败,则返回对应的错误。接着调用 publish 方法来执行真正的发布逻辑,如果发布失败,则返回对应的错误。最后,返回发布的帖子ID。

func (f *PublishPostFlow) checkParam() error {
	if f.userId <= 0 {
		return errors.New("userId id must be larger than 0")
	}
	if utf8.RuneCountInString(f.content) >= 500 {
		return errors.New("content length must be less than 500")
	}
	return nil
}

checkParam 方法用于检查参数的合法性。在该方法中,首先检查 userId 是否大于0,如果小于等于0,则返回对应的错误。接着,使用 utf8.RuneCountInString 函数来检查帖子内容的长度是否小于500个字符,如果超过500个字符,则返回对应的错误。如果参数校验通过,则返回 nil

func (f *PublishPostFlow) publish() error {
	post := &repository.Post{
		ParentId:   f.topicId,
		UserId:     f.userId,
		Content:    f.content,
		CreateTime: time.Now(),
	}
	if err :=	repository.NewPostDaoInstance().CreatePost(post); err != nil {
		return err
	}
	f.postId = post.Id
	return nil
}

publish 方法用于执行实际的帖子发布逻辑。在该方法中,首先创建一个 repository.Post 对象,并将发布帖子所需的属性赋值给该对象的对应字段,其中 ParentId 表示帖子所属的主题ID,UserId 表示发布帖子的用户ID,Content 表示帖子的内容,CreateTime 表示帖子的创建时间。

接着,调用 repository.NewPostDaoInstance() 创建一个 PostDao 对象,并调用其 CreatePost 方法来将帖子对象插入数据库。如果插入过程中发生错误,则返回对应的错误。

最后,将插入成功后的帖子ID赋值给 f.postId 属性,并返回 nil 表示发布成功。

package service

import (
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"github.com/Moonlight-Zhao/go-project-example/util"
	"os"
	"testing"
)

func TestMain(m *testing.M) {
	if err := repository.Init(); err != nil {
		os.Exit(1)
	}
	if err := util.InitLogger(); err != nil {
		os.Exit(1)
	}
	m.Run()
}

func TestPublishPost(t *testing.T) {

	type args struct {
		topicId int64
		userId  int64
		content string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "测试发布回帖",
			args: args{
				topicId: 1,
				userId:  2,
				content: "再次回帖",
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := PublishPost(tt.args.topicId, tt.args.userId, tt.args.content)
			if (err != nil) != tt.wantErr {
				t.Errorf("PublishPost() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}
func TestMain(m *testing.M) {
	if err := repository.Init(); err != nil {
		os.Exit(1)
	}
	if err := util.InitLogger(); err != nil {
		os.Exit(1)
	}
	m.Run()
}

TestMain 函数是测试包的入口函数,用于初始化测试环境。在该函数中,首先调用 repository.Init() 初始化数据库连接,如果初始化过程中发生错误,则调用 os.Exit(1) 终止测试执行。接着调用 util.InitLogger() 初始化日志记录器,如果初始化过程中发生错误,则同样调用 os.Exit(1) 终止测试执行。最后调用 m.Run() 来运行测试函数。

func TestPublishPost(t *testing.T) {
	// 测试参数定义
	type args struct {
		topicId int64
		userId  int64
		content string
	}
	// 测试用例定义
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "测试发布回帖",
			args: args{
				topicId: 1,
				userId:  2,
				content: "再次回帖",
			},
			wantErr: false,
		},
	}
	// 遍历测试用例
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// 调用被测试函数
			_, err := PublishPost(tt.args.topicId, tt.args.userId, tt.args.content)
			// 检查错误是否符合预期
			if (err != nil) != tt.wantErr {
				t.Errorf("PublishPost() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

TestPublishPost 函数是用于测试 PublishPost 函数的单元测试函数。在该函数中,首先定义了一个 args 结构体来表示测试参数,其中包括 topicIduserIdcontent。接着定义了一个测试用例切片 tests,其中包含了一个测试用例。测试用例中定义了一个名称、测试参数和预期的错误结果。

然后通过遍历测试用例切片,逐个运行测试用例。在每个测试用例中,调用 PublishPost 函数,并将测试参数传入。然后检查返回的错误结果是否符合预期,如果不符合预期,则使用 t.Errorf 输出错误信息。

这样,通过单元测试可以验证 PublishPost 函数在给定参数下的行为是否符合预期。

package service

import (
	"errors"
	"fmt"
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"sync"
)

type TopicInfo struct {
	Topic *repository.Topic
	User  *repository.User
}

type PostInfo struct {
	Post *repository.Post
	User *repository.User
}

type PageInfo struct {
	TopicInfo *TopicInfo
	PostList  []*PostInfo
}

func QueryPageInfo(topicId int64) (*PageInfo, error) {
	return NewQueryPageInfoFlow(topicId).Do()
}

func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
	return &QueryPageInfoFlow{
		topicId: topId,
	}
}

type QueryPageInfoFlow struct {
	topicId  int64
	pageInfo *PageInfo

	topic   *repository.Topic
	posts   []*repository.Post
	userMap map[int64]*repository.User
}

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
}

func (f *QueryPageInfoFlow) checkParam() error {
	if f.topicId <= 0 {
		return errors.New("topic id must be larger than 0")
	}
	return nil
}

func (f *QueryPageInfoFlow) prepareInfo() error {
	//获取topic信息
	var wg sync.WaitGroup
	wg.Add(2)
	var topicErr, postErr error
	go func() {
		defer wg.Done()
		topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
		if err != nil {
			topicErr = err
			return
		}
		f.topic = topic
	}()
	//获取post列表
	go func() {
		defer wg.Done()
		posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId)
		if err != nil {
			postErr = err
			return
		}
		f.posts = posts
	}()
	wg.Wait()
	if topicErr != nil {
		return topicErr
	}
	if postErr != nil {
		return postErr
	}
	//获取用户信息
	uids := []int64{f.topic.Id}
	for _, post := range f.posts {
		uids = append(uids, post.Id)
	}
	userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
	if err != nil {
		return err
	}
	f.userMap = userMap
	return nil
}

func (f *QueryPageInfoFlow) packPageInfo() error {
	//topic info
	userMap := f.userMap
	topicUser, ok := userMap[f.topic.UserId]
	if !ok {
		return errors.New("has no topic user info")
	}
	//post list
	postList := make([]*PostInfo, 0)
	for _, post := range f.posts {
		postUser, ok := userMap[post.UserId]
		if !ok {
			return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
		}
		postList = append(postList, &PostInfo{
			Post: post,
			User: postUser,
		})
	}
	f.pageInfo = &PageInfo{
		TopicInfo: &TopicInfo{
			Topic: f.topic,
			User:  topicUser,
		},
		PostList: postList,
	}
	return nil
}
func TestMain(m *testing.M) {
	if err := repository.Init(); err != nil {
		os.Exit(1)
	}
	if err := util.InitLogger(); err != nil {
		os.Exit(1)
	}
	m.Run()
}

TestMain 函数是测试包的入口函数,用于初始化测试环境。在该函数中,首先调用 repository.Init() 初始化数据库连接,如果初始化过程中发生错误,则调用 os.Exit(1) 终止测试执行。接着调用 util.InitLogger() 初始化日志记录器,如果初始化过程中发生错误,则同样调用 os.Exit(1) 终止测试执行。最后调用 m.Run() 来运行测试函数。

func TestPublishPost(t *testing.T) {
	// 测试参数定义
	type args struct {
		topicId int64
		userId  int64
		content string
	}
	// 测试用例定义
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "测试发布回帖",
			args: args{
				topicId: 1,
				userId:  2,
				content: "再次回帖",
			},
			wantErr: false,
		},
	}
	// 遍历测试用例
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// 调用被测试函数
			_, err := PublishPost(tt.args.topicId, tt.args.userId, tt.args.content)
			// 检查错误是否符合预期
			if (err != nil) != tt.wantErr {
				t.Errorf("PublishPost() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

TestPublishPost 函数是用于测试 PublishPost 函数的单元测试函数。在该函数中,首先定义了一个 args 结构体来表示测试参数,其中包括 topicIduserIdcontent。接着定义了一个测试用例切片 tests,其中包含了一个测试用例。测试用例中定义了一个名称、测试参数和预期的错误结果。

然后通过遍历测试用例切片,逐个运行测试用例。在每个测试用例中,调用 PublishPost 函数,并将测试参数传入。然后检查返回的错误结果是否符合预期,如果不符合预期,则使用 t.Errorf 输出错误信息。

这样,通过单元测试可以验证 PublishPost 函数在给定参数下的行为是否符合预期。

package service

import (
	"errors"
	"fmt"
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"sync"
)

type TopicInfo struct {
	Topic *repository.Topic
	User  *repository.User
}

type PostInfo struct {
	Post *repository.Post
	User *repository.User
}

type PageInfo struct {
	TopicInfo *TopicInfo
	PostList  []*PostInfo
}

func QueryPageInfo(topicId int64) (*PageInfo, error) {
	return NewQueryPageInfoFlow(topicId).Do()
}

func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
	return &QueryPageInfoFlow{
		topicId: topId,
	}
}

type QueryPageInfoFlow struct {
	topicId  int64
	pageInfo *PageInfo

	topic   *repository.Topic
	posts   []*repository.Post
	userMap map[int64]*repository.User
}

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
}

func (f *QueryPageInfoFlow) checkParam() error {
	if f.topicId <= 0 {
		return errors.New("topic id must be larger than 0")
	}
	return nil
}

func (f *QueryPageInfoFlow) prepareInfo() error {
	//获取topic信息
	var wg sync.WaitGroup
	wg.Add(2)
	var topicErr, postErr error
	go func() {
		defer wg.Done()
		topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
		if err != nil {
			topicErr = err
			return
		}
		f.topic = topic
	}()
	//获取post列表
	go func() {
		defer wg.Done()
		posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId)
		if err != nil {
			postErr = err
			return
		}
		f.posts = posts
	}()
	wg.Wait()
	if topicErr != nil {
		return topicErr
	}
	if postErr != nil {
		return postErr
	}
	//获取用户信息
	uids := []int64{f.topic.Id}
	for _, post := range f.posts {
		uids = append(uids, post.Id)
	}
	userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
	if err != nil {
		return err
	}
	f.userMap = userMap
	return nil
}

func (f *QueryPageInfoFlow) packPageInfo() error {
	//topic info
	userMap := f.userMap
	topicUser, ok := userMap[f.topic.UserId]
	if !ok {
		return errors.New("has no topic user info")
	}
	//post list
	postList := make([]*PostInfo, 0)
	for _, post := range f.posts {
		postUser, ok := userMap[post.UserId]
		if !ok {
			return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
		}
		postList = append(postList, &PostInfo{
			Post: post,
			User: postUser,
		})
	}
	f.pageInfo = &PageInfo{
		TopicInfo: &TopicInfo{
			Topic: f.topic,
			User:  topicUser,
		},
		PostList: postList,
	}
	return nil
}
type TopicInfo struct {
	Topic *repository.Topic
	User  *repository.User
}

type PostInfo struct {
	Post *repository.Post
	User *repository.User
}

type PageInfo struct {
	TopicInfo *TopicInfo
	PostList  []*PostInfo
}

在这段代码中定义了三个结构体:TopicInfoPostInfoPageInfoTopicInfo 结构体包含了一个指向 repository.Topic 的指针和一个指向 repository.User 的指针。PostInfo 结构体包含了一个指向 repository.Post 的指针和一个指向 repository.User 的指针。PageInfo 结构体包含了一个指向 TopicInfo 的指针和一个 PostInfo 切片。

func QueryPageInfo(topicId int64) (*PageInfo, error) {
	return NewQueryPageInfoFlow(topicId).Do()
}

QueryPageInfo 函数用于查询页面信息,接受一个 topicId 参数,返回一个 PageInfo 结构体指针和一个错误。在函数内部,它通过调用 NewQueryPageInfoFlow(topicId) 创建一个 QueryPageInfoFlow 对象,并调用其 Do 方法来执行查询并返回结果。

func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
	return &QueryPageInfoFlow{
		topicId: topId,
	}
}

NewQueryPageInfoFlow 函数用于创建一个 QueryPageInfoFlow 对象,并接收一个 topId 参数来设置其 topicId 字段,然后返回该对象的指针。

type QueryPageInfoFlow struct {
	topicId  int64
	pageInfo *PageInfo

	topic   *repository.Topic
	posts   []*repository.Post
	userMap map[int64]*repository.User
}

QueryPageInfoFlow 结构体定义了一个查询页面信息的流程。它包含了 topicIdpageInfotopicpostsuserMap 字段。其中,topicId 表示要查询的话题的 ID,pageInfo 表示查询结果的页面信息,topic 表示查询到的话题对象,posts 表示查询到的帖子对象列表,userMap 是一个用户 ID 到用户对象的映射。

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
}

Do 方法用于执行查询页面信息的流程。首先调用 checkParam 方法检查参数的有效性,如果参数无效,则返回相应的错误。然后调用 prepareInfo 方法准备查询所需的信息,如果准备过程中出现错误,则返回相应的错误。最后调用 packPageInfo 方法打包页面信息,如果打包过程中出现错误,则返回相应的错误。如果所有步都成功完成,则返回查询到的页面信息 f.pageInfo 和 nil 错误。

goCopy code
func (f *QueryPageInfoFlow) checkParam() error { if f.topicId <= 0 { returnerrors.New("topic id must be larger than 0") } return nil }

checkParam 方法用于检查参数的有效性。如果 topicId 小于等于 0,则返回一个错误,表示话题 ID 必须大于 0。

goCopy code
func (f *QueryPageInfoFlow) prepareInfo() error { //获取topic信息 var wg sync.WaitGroup wg.Add(2) var topicErr, postErr error go func() { defer wg.Done() topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId) if err != nil { topicErr = errreturn } f.topic = topic }() //获取post列表 go func() { defer wg.Done() posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId) if err != nil { postErr = err return } f.posts = posts }() wg.Wait() if topicErr != nil { return topicErrifpostErr != nil { return postErr } //获取用户信息 uids := []int64{f.topic.Idfor _, post := range f.posts { uidsappend(uids, post.Id) } userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids) if err != nil { return err } f.userMap = userMap return nil }

prepareInfo 方法用于准备查询所需的信息。它并发地获取话题信息和帖子列表,并等待它们的完成。首先,通过调用 repository.NewTopicDaoInstance().QueryTopicById(f.topicId) 获取指定话题 ID 的话题信息,并将其赋值给 f.topic。然后,通过调用 repository.NewPostDaoInstance().QueryPostByParentId(f.topicId) 获取指定话题 ID 的帖子列表,并将其赋值给 f.posts。同时使用一个 WaitGroup 来等待两个并发操作的完成。

接下来,检查获取话题信息和帖子列表过程中是否出现错误。如果出现错误,则返回相应的错误。否则,从话题和帖子中提取出所有的用户 ID,并将它们存储在 uids 切片中。然后,调用 repository.NewUserDaoInstance().MQueryUserById(uids) 并传入用户 ID 切片,以获取所有用户的详细信息,并将其存储在 f.userMap 中,其中键是用户 ID,值是用户对象。

最后,如果获取用户信息的过程中出现错误,则返回相应的错误;否则,返回 nil,表示准备信息的过程顺利完成。

postList := make([]*PostInfo, 0)
for _, post := range f.posts {
	postUser, ok := userMap[post.UserId]
	if !ok {
		return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
	}
	postList = append(postList, &PostInfo{
		Post: post,
		User: postUser,
	})
}
f.pageInfo = &PageInfo{
	TopicInfo: &TopicInfo{
		Topic: f.topic,
		User:  topicUser,
	},
	PostList: postList,
}
return nil

packPageInfo 方法用于打包页面信息。首先,创建一个空的 postList 切片。然后,遍历帖子列表 f.posts,对于每个帖子,从 userMap 中获取对应的用户信息 postUser。如果获取不到用户信息,则返回一个错误,表示找不到对应的用户信息。

接下来,将帖子信息和对应的用户信息组合成一个 PostInfo 对象,并将其添加到 postList 中。

最后,创建一个 PageInfo 对象,其中包含了话题信息和帖子列表信息。将它赋值给 f.pageInfo,并返回 nil,表示页面信息的打包过程成功完成。

以上就是代码中各个函数和方法的逐行解释。它们主要用于发布帖子、查询页面信息等功能的实现。

package service

import (
	"testing"
)

func TestQueryPageInfo(t *testing.T) {
	type args struct {
		topicId int64
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "查询页面",
			args: args{
				topicId: 1,
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := QueryPageInfo(tt.args.topicId)
			if (err != nil) != tt.wantErr {
				t.Errorf("QueryPageInfo() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

这是一个测试函数 TestQueryPageInfo,用于测试 QueryPageInfo 函数。在测试中,定义了一个 args 结构,包含了待查询的话题 ID。然后,创建了一个测试案例的切片 tests,其中包含了一个测试案例。

在该测试案例中,给定了一个话题 ID,并期望不会发生错误。

接下来,通过 for 循环遍历测试案例切片,对每个测试案例进行单独的测试。在每个测试中,调用 QueryPageInfo 函数,并将返回的结果和期望的错误进行比较。如果返回的错误与期望的错误不一致,则输出测试失败的信息。

这个测试函数用于验证 QueryPageInfo 函数的功能是否正确,确保在给定合法的话题 ID 时能够正常查询页面信息,并且不会返回错误。

package util

import "go.uber.org/zap"

var Logger *zap.Logger

func InitLogger() error {
	var err error
	Logger, err = zap.NewProduction()
	if err != nil {
		return err
	}
	return nil
}
import "go.uber.org/zap"

var Logger *zap.Logger

func InitLogger() error {
	var err error
	Logger, err = zap.NewProduction()
	if err != nil {
		return err
	}
	return nil
}

这段代码定义了一个日志记录器的全局变量 Logger,它使用了 Uber 开源的 Zap 日志库。该变量的类型是 *zap.Logger

函数 InitLogger() 用于初始化日志记录器。它首先声明一个 err 变量用于存储可能发生的错误。然后,调用 zap.NewProduction() 函数创建一个生产环境下的 Zap 日志记录器,并将返回的日志记录器赋值给全局变量 Logger。如果创建过程中发生错误,将错误赋值给 err 变量,并返回该错误。

通过调用 InitLogger() 函数,可以初始化全局的日志记录器 Logger,使其可以在整个应用程序中使用。这样,其他模块和函数就可以通过 Logger 变量进行日志记录操作。

package main

import (
	"github.com/Moonlight-Zhao/go-project-example/handler"
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"github.com/Moonlight-Zhao/go-project-example/util"
	"gopkg.in/gin-gonic/gin.v1"
	"os"
)

func main() {
	if err := Init(); err != nil {
		os.Exit(-1)
	}
	r := gin.Default()

	r.Use(gin.Logger())

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")
		data := handler.QueryPageInfo(topicId)
		c.JSON(200, data)
	})

	r.POST("/community/post/do", func(c *gin.Context) {
		uid, _ := c.GetPostForm("uid")
		topicId, _ := c.GetPostForm("topic_id")
		content, _ := c.GetPostForm("content")
		data := handler.PublishPost(uid, topicId, content)
		c.JSON(200, data)
	})
	err := r.Run()
	if err != nil {
		return
	}
}

func Init() error {
	if err := repository.Init(); err != nil {
		return err
	}
	if err := util.InitLogger(); err != nil {
		return err
	}
	return nil
}
import (
	"github.com/Moonlight-Zhao/go-project-example/handler"
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"github.com/Moonlight-Zhao/go-project-example/util"
	"gopkg.in/gin-gonic/gin.v1"
	"os"
)

func main() {
	if err := Init(); err != nil {
		os.Exit(-1)
	}
	r := gin.Default()

	r.Use(gin.Logger())

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")
		data := handler.QueryPageInfo(topicId)
		c.JSON(200, data)
	})

	r.POST("/community/post/do", func(c *gin.Context) {
		uid, _ := c.GetPostForm("uid")
		topicId, _ := c.GetPostForm("topic_id")
		content, _ := c.GetPostForm("content")
		data := handler.PublishPost(uid, topicId, content)
		c.JSON(200, data)
	})
	err := r.Run()
	if err != nil {
		return
	}
}

func Init() error {
	if err := repository.Init(); err != nil {
		return err
	}
	if err := util.InitLogger(); err != nil {
		return err
	}
	return nil
}

这段代码是应用程序的入口函数 main() 和初始化函数 Init()

main() 函数中,首先调用 Init() 函数来初始化应用程序的依赖项。如果初始化过程中发生错误,将通过 os.Exit(-1) 来退出应用程序。

然后创建一个默认的 Gin 路由实例 r := gin.Default(),该实例将用于处理 HTTP 请求。

通过 r.Use(gin.Logger()) 将 Gin 的日志中间件添加到路由中,以记录每个请求的日志。

定义了三个路由处理函数:

  1. GET 请求 "/ping",返回 JSON 格式的响应 {"message": "pong"}
  2. GET 请求 "/community/page/get/:id",根据 URL 中的参数 "id" 调用 handler.QueryPageInfo() 函数,并将返回的数据以 JSON 格式响应给客户端。
  3. POST 请求 "/community/post/do",从 POST 表单中获取 "uid"、"topic_id" 和 "content",然后调用 handler.PublishPost() 函数处理发布帖子的逻辑,并将返回的数据以 JSON 格式响应给客户端。

最后,通过调用 r.Run() 启动应用程序的 HTTP 服务,监听并处理传入的请求。

Init() 函数用于初始化应用程序的依赖项。它首先调用 repository.Init() 初始化数据库连接和数据存储相关的内容,然后调用 util.InitLogger() 初始化日志记录器。如果初始化过程中发生错误,将返回该错误。