手把手教学-Go语言工程实践-社区发帖项目
不是开玩笑的,本文只要你愿意跟着抄,基本就能把课上的东西弄明白抄一遍🐶,实操写的非常非常详细。如果你看不明白,到下方github仓库提交issue,我来帮你解决问题🕊
如果这篇文章对你有帮助,希望你能留下点赞,如果你能到github点个star就更好了(@^0^@)/。
本文所有案例代码都可以在这里获取: MoFishXiaodui/ExecutableManual: 青训营后端-可执行手册-非常详细的手把手教学教程 (github.com)
案例14 - 克隆老师的社区话题页面案例进行测试
因为这是一个完整的仓库,且此次案例不需要我们写代码,所以这里只放链接,就不把仓库代码放到这里了。
Moonlight-Zhao/go-project-example at V0 (github.com)
注意,我们只用分支 V0 上面的代码,主分支的代码需要用到数据库,不方便初学者测试。大家主要克隆代码时只克隆V0上的代码就行。
你可以克隆主分支后再切到V0分支,然后拉取V0分支。
也可以直接只克隆V0分支
附上GPT的解答:
问:git clone 如何只克隆特定分支?
ChatGPT:
要克隆一个特定分支而不是整个仓库,可以使用Git的-b选项来指定要克隆的分支。以下是在git clone命令中使用-b选项的示例:
git clone -b branch_name repository_url
其中:
branch_name是你要克隆的特定分支的名称。repository_url是你要克隆的远程仓库的URL。
例如,如果你想克隆一个名为development的特定分支,仓库的URL为https://github.com/example/repo.git,你可以执行以下命令:
git clone -b development https://github.com/example/repo.git
这将只克隆development分支的内容到你的本地目录中,而不会包含其他分支和提交历史。
开始实操
-
找一个合适的位置,执行
git clone -b V0 git@github.com:Moonlight-Zhao/go-project-example.git -
然后在项目根目录上执行
go run server.go -
当看到终端输出
[GIN-debug] Listening and serving HTTP on :8080时,即为启动成功。 -
先去浏览器查看
localhost:8080,可以看到404,这是因为代码没有配置根路由的处理函数,默认404了。 -
为了找到有用的路由,我们可以到
server.go中查看gin的路由处理函数r.GET("/community/page/get/:id", func(c *gin.Context) { topicId := c.Param("id") data := cotroller.QueryPageInfo(topicId) c.JSON(200, data) }) -
从上面的代码中,可以看到gin引擎r可以处理路径为
/community/page/get/:id的GET请求。于是尝试在浏览器访问:localhost:8080/community/page/get/1,既可以看到响应数据,然后请自行把id换成2和3等进行测试。 -
这些数据来源于根目录data文件夹下的topic文件。请尝试修改以及增加数据。
-
当你修改了文件夹的数据后,刷新浏览器并不能直接看到新数据,需要你关掉服务再执行命令
go run server.go开启服务
案例15 - 动手复刻社区话题页面案例
此次案例代码代码量颇多,分多个章节进行。
初始化项目
- 新建文件夹
2-2-15web作为项目的根目录(本案例中说的根目录都是指这个文件夹下) - 在
2-2-15web目录下执行go mod init web
构建data和repository层
说明
- data 存放数据本身
- repository 放直接操作data数据的代码,形成模型(model)供上层业务调用
-
在项目根目录新建文件夹 data 和 repository
-
在data里面新建文件topic,写入如下数据(单独每一行作json数据),但并非整个文件作为json文件
{"id":1, "title":"青训营来啦1", "content":"冲冲冲!", "create_time":"20230804150505"} {"id":2, "title":"青训营来啦2", "content":"冲冲冲!", "create_time":"20230804150506"} {"id":3, "title":"青训营来啦3", "content":"冲冲冲!", "create_time":"20230804150605"} {"id":4, "title":"青训营来啦4", "content":"冲冲冲!", "create_time":"20230804150705"} -
在repository文件夹下新建文件topic.go,这里存放操作topic数据的代码
- 在topic.go内部写入代码如下
-
// 注意包名是repository package repository // 1-先声明一个结构体,json导出和 data/topic 的内容 对应上 type Topic struct { Id int64 `json:"id"` Title string `json:"title"` Content string `json:"content"` CreateTime string `json:"create_time"` } // 2-创建索引表。可以快速通过Id值找到对应的Topic var topicIndexMap map[int64]*Topic // 3-InitTopicIndexMap 初始化话题索引 // 如果要规范的话,后面要设置成不可导出(即小写字母开头,但是为了方便测试,先用着大写字母开头) func InitTopicIndexMap(filePath string) error { // 尝试打开文件 file, err := os.Open(filePath + "topic") if err != nil { return err } scanner := bufio.NewScanner(file) // 为临时topic索引分配空间 topicTmpMap := make(map[int64]*Topic) fmt.Println("开始读取数据") // 读取文本 for scanner.Scan() { text := scanner.Text() fmt.Println(text) // 新建单个Topic结构 var topic Topic // 转换text填充数据到topic上 if err := json.Unmarshal([]byte(text), &topic); err != nil { return err } // 把topic指针添加到临时索引表上 topicTmpMap[topic.Id] = &topic } // 把 临时索引表 赋给 最终的索引表 topicIndexMap = topicTmpMap return nil }
-
为了能够测试上面代码的正确性,提供一下两个方法进行判断,建议先用方法一,后面再用方法二。因为方法二不能看到读取文件的过程。
-
方法1
-
在根目录新建test文件夹,再在内新建testRepositoryTopic.go文件
-
写入代码如下
package main import ( "fmt" // 这个web来自于go mod init时写的模块名,repository就是根目录下的repository包 "web/repository" ) func main() { err := repository.InitTopicIndexMap("../data/") fmt.Println("err", err) } -
然后直接运行,会逐行输出读取的内容,说明写成功了
(base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-2-15web\test> go run .\testRepositoryTopic.go 开始读取数据 {"id":1, "title":"青训营来啦1", "content":"冲冲冲!", "create_time":"20230804150505"} {"id":2, "title":"青训营来啦2", "content":"冲冲冲!", "create_time":"20230804150506"} {"id":3, "title":"青训营来啦3", "content":"冲冲冲!", "create_time":"20230804150605"} {"id":4, "title":"青训营来啦4", "content":"冲冲冲!", "create_time":"20230804150705"} err <nil>
-
-
方法二
-
在根目录执行
go get "github.com/stretchr/testify/assert"添加包 -
在repository文件夹下新建topic_test.go文件,写入以下代码
package repository import ( "testing" "github.com/stretchr/testify/assert" ) func TestInitTopicIndexMap(t *testing.T) { var expect error = nil output := InitTopicIndexMap("../data/") assert.Equal(t, expect, output) } -
在根目录下执行
go test .\repository可以看到测试结果。
-
-
-
有关topic的repository层到此就做完了 -
repository层还需要对外提供Dao实体,需要在topic.go文件添加如下代码
// 4-对外提供Dao模型
type TopicDao struct {
}
// 5-设置锁
var (
topicDao *TopicDao
topicOnce sync.Once
)
// 6-定义 TopicDao 初始函数
func NewTopicDaoInstance() *TopicDao {
// func (*sync.Once).Do(f func())
// Once is an object that will perform exactly one action.
// A Once must not be copied after first use.
topicOnce.Do(
func() {
// 创建TopicDao实例并把该实例的指针赋值给全局的topicDao
topicDao = &TopicDao{}
})
return topicDao
}
// 7-定义topic查询函数
func (*TopicDao) QueryTopicById(id int64) *Topic {
return topicIndexMap[id]
}
构建service层
先看我画的这张草图,了解我们的service层是怎样与联系controller层和repository层的
看完这个,可以直接开始看代码
- 在根目录下新建service文件夹,再在内新建文件
query_page_info.go - 在文件
query_page_info.go,该文件包名为service,然后导入包`import "web/repository" - 写入以下代码
package service
import (
"errors"
"sync"
"web/repository"
)
// PageInfo是最终要返回给上层(controller)函数的实体
type PageInfo struct {
Topic *repository.Topic
}
// 处理请求内容和生成数据的结构体
type QueryPageInfoFlow struct {
// 接收上层传来的topicId
topicId int64
// 组装的PageInfo实体,用来返回给上层
pageInfo *PageInfo
// 下面是获取散装的模型数据
topic *repository.Topic // topic
// 以后再添加 post 模型
}
// 用来检查参数的函数
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 {
var wg sync.WaitGroup
wg.Add(1)
// 获取topic信息
go func() {
defer wg.Done()
topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
f.topic = topic
}()
// 获取post信息先不写
wg.Wait()
return nil
}
// 用来组装成PageInfo实体的函数
func (f *QueryPageInfoFlow) preparePageInfo() error {
f.pageInfo = &PageInfo{
Topic: f.topic,
}
return nil
}
// 外部来了一个请求,我们创建一个新的QueryPageInfoFlow结构来处理,这个结构的Do()方法最终返回想要的结果
func QueryPageInfo(topicId int64) (*PageInfo, error) {
return NewQueryPageInfoFlow(topicId).Do()
}
// 创建新QueryPageInfoFlow实例的函数
func NewQueryPageInfoFlow(topicId int64) *QueryPageInfoFlow {
return &QueryPageInfoFlow{
topicId: topicId,
}
}
// Do函数封装了检查参数、获取数据、组装实体三个步骤,并返回PageInfo
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.preparePageInfo(); err != nil {
return nil, err
}
return f.pageInfo, nil
}
4. 在service文件夹下新建一个测试文件query_page_info_test.go,在内写入文件:
package service
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
"web/repository"
)
func TestMain(m *testing.M) {
repository.InitTopicIndexMap("../data/")
os.Exit(m.Run())
}
func TestQueryPageInfo(t *testing.T) {
pageInfo, _ := QueryPageInfo(1)
assert.Equal(
t,
PageInfo{Topic: &repository.Topic{Content: "冲冲冲!"}}.Topic.Content,
pageInfo.Topic.Content,
)
}
5. 在根目录执行命令 go test service进行测试,然后把冲冲冲换成冲冲冲!(中文感叹号)再测试一次
# 冲冲冲
(base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-2-15web> go test .\service\
开始读取数据
{"id":1, "title":"青训营来啦1", "content":"冲冲冲!", "create_time":"20230804150505"}
{"id":2, "title":"青训营来啦2", "content":"冲冲冲!", "create_time":"20230804150506"}
{"id":3, "title":"青训营来啦3", "content":"冲冲冲!", "create_time":"20230804150605"}
{"id":4, "title":"青训营来啦4", "content":"冲冲冲!", "create_time":"20230804150705"}
--- FAIL: TestQueryPageInfo (0.00s)
query_page_info_test.go:18:
Error Trace: D:/code/MoFishXiaodui/ExecutableManual/src/2-2-15web/service/query_page_info_test.go:18
Error: Not equal:
expected: "冲冲冲"
actual : "冲冲冲!"
--- Expected
+++ Actual
@@ -1 +1 @@
-冲冲冲
+冲冲冲!
Test: TestQueryPageInfo
FAIL
FAIL web/service 0.219s
FAIL
# 冲冲冲!
(base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-2-15web> go test .\service\
ok web/service (cached)
# 测试成功
构建controller层
本层代码提供最后一层的封装控制。本次案例涉及逻辑不多,此次controller层仅是封装一个 通过string类型的topicId获取topic 的函数,逻辑不复杂。读者认真完成service层为关键。
-
在根目录新建controller文件夹,再在内新建
query_page_info.go文件(与service层的文件同名,你若改成其他文件名当然也是可以的)。 -
在
query_page_info.go内书写以下代码package controller import ( "strconv" "web/service" ) type PageData struct { Code int64 `json:"code"` // 状态码 Msg string `json:"msg"` // 成功/错误信息 Data interface{} `json:"data"` // 数据 } func QueryPageInfo(topicIdStr string) *PageData { // 将string类型的topicIdStr转换为int64类型的topicId topicId, err := strconv.ParseInt(topicIdStr, 10, 64) if err != nil { return &PageData{ Code: -1, Msg: err.Error(), } } pageInfo, err := service.QueryPageInfo(topicId) if err != nil { return &PageData{ Code: -1, Msg: err.Error(), } } return &PageData{ Code: 0, Msg: "success", Data: pageInfo, } } -
新建测试文件
query_page_info_test.go中书写以下代码package controller import ( "github.com/stretchr/testify/assert" "os" "testing" "web/repository" ) func TestMain(m *testing.M) { repository.InitTopicIndexMap("../data/") os.Exit(m.Run()) } func TestQueryPageInfo(t *testing.T) { pageData := QueryPageInfo("1") assert.Equal( t, int64(0), pageData.Code, ) } -
测试方法不再赘述。在TestQueryPageInfo中可以修改 int64() 内的值进行测试
# int64(0) (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-2-15web> go test .\controller\ ok web/controller 0.224s # int64(-1) (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-2-15web> go test .\controller\ 开始读取数据 {"id":1, "title":"青训营来啦1", "content":"冲冲冲!", "create_time":"20230804150505"} {"id":2, "title":"青训营来啦2", "content":"冲冲冲!", "create_time":"20230804150506"} {"id":3, "title":"青训营来啦3", "content":"冲冲冲!", "create_time":"20230804150605"} {"id":4, "title":"青训营来啦4", "content":"冲冲冲!", "create_time":"20230804150705"} --- FAIL: TestQueryPageInfo (0.00s) query_page_info_test.go:18: Error Trace: D:/code/MoFishXiaodui/ExecutableManual/src/2-2-15web/controller/query_page_info_test.go:18 Error: Not equal: expected: -1 actual : 0 Test: TestQueryPageInfo FAIL FAIL web/controller 0.214s FAIL
提供Web服务
我们本次实验使用gin框架来提供web服务。
如果你在下文中尝试使用gin框架有困难,可以看我的另一篇文章手把手教学-Gin框架入门 | 青训营 - 掘金 (juejin.cn)学习Gin的基础用法。
-
在根目录新建
server.go文件,然后去看上面的Gin框架文档的入门指南 -
在根目录执行安装命令
go get -u github.com/gin-gonic/gin -
把Gin快速案例代码copy到
server.go上面,然后直接运行package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() // 路由器url - ping r.GET("/ping", func(c *gin.Context) { // - 200 是请求成功状态码,问就是规定200是成功,404是page not found // - type gin.H map[string]any // c.JSON 快速使用map或者struct返回json数据 // c.JSON serializes the given struct as JSON into the response body. // It also sets the Content-Type as "application/json". c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // 监听并在 0.0.0.0:8080 上启动服务 } -
导入repository包,书写数据初始化的代码
import "web/repository" func main() { err := repository.InitTopicIndexMap("./data/") if err != nil { fmt.Println("初始化数据出错") os.Exit(-1) } /* ... */ } -
导入controller包,仿照第一个r.GET代码,书写自己逻辑代码
r.GET("/topic/:id", func(c *gin.Context) { // 获取id参数 topicId := c.Param("id") // 通过controller层的QueryPageInfo函数找Topic相关数据 data := controller.QueryPageInfo(topicId) // 直接把数据返回 c.JSON(200, data) }) // r.Run() // 默认监听并在 0.0.0.0:8080 上启动服务 r.Run(":9000") // 指定在9000端口上 启动服务 -
直接运行进行测试
-
在浏览器依次测试以下链接
到此,有关topic内容的查询服务 从底部到web服务 都搭建好了。
接下来完善有关post内容的查询服务。如果大家看不懂实践不顺利来催更我再补充后续内容。实现代码已经在github上有了。