手把手教学-Go语言工程实践-社区发帖项目 | 青训营

1,004 阅读11分钟

手把手教学-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分支的内容到你的本地目录中,而不会包含其他分支和提交历史。

开始实操

  1. 找一个合适的位置,执行 git clone -b V0 git@github.com:Moonlight-Zhao/go-project-example.git

  2. 然后在项目根目录上执行go run server.go

  3. 当看到终端输出[GIN-debug] Listening and serving HTTP on :8080时,即为启动成功。

  4. 先去浏览器查看localhost:8080,可以看到404,这是因为代码没有配置根路由的处理函数,默认404了。

  5. 为了找到有用的路由,我们可以到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)
    })
    
  6. 从上面的代码中,可以看到gin引擎r可以处理路径为 /community/page/get/:id的GET请求。于是尝试在浏览器访问:localhost:8080/community/page/get/1,既可以看到响应数据,然后请自行把id换成2和3等进行测试。

  7. 这些数据来源于根目录data文件夹下的topic文件。请尝试修改以及增加数据。

  8. 当你修改了文件夹的数据后,刷新浏览器并不能直接看到新数据,需要你关掉服务再执行命令go run server.go开启服务

案例15 - 动手复刻社区话题页面案例

此次案例代码代码量颇多,分多个章节进行。

初始化项目

  1. 新建文件夹 2-2-15web作为项目的根目录(本案例中说的根目录都是指这个文件夹下)
  2. 2-2-15web目录下执行go mod init web

构建data和repository层

说明

  • data 存放数据本身
  • repository 放直接操作data数据的代码,形成模型(model)供上层业务调用
  1. 在项目根目录新建文件夹 data 和 repository

  2. 在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"}
    
  3. 在repository文件夹下新建文件topic.go,这里存放操作topic数据的代码

    1. 在topic.go内部写入代码如下
    2.    // 注意包名是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
         }
      
  4. 为了能够测试上面代码的正确性,提供一下两个方法进行判断,建议先用方法一,后面再用方法二。因为方法二不能看到读取文件的过程。

    1. 方法1

      1. 在根目录新建test文件夹,再在内新建testRepositoryTopic.go文件

      2. 写入代码如下

        package main
        ​
        import (
            "fmt"
            
            // 这个web来自于go mod init时写的模块名,repository就是根目录下的repository包
            "web/repository"
        )
        ​
        func main() {
            err := repository.InitTopicIndexMap("../data/")
            fmt.Println("err", err)
        }
        
      3. 然后直接运行,会逐行输出读取的内容,说明写成功了

        (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>
        
    2. 方法二

      1. 在根目录执行 go get "github.com/stretchr/testify/assert"添加包

      2. 在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)
        }
        
      3. 在根目录下执行 go test .\repository可以看到测试结果。

  5. 有关topic的repository层到此就做完了

  6. 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层.png

看完这个,可以直接开始看代码

  1. 在根目录下新建service文件夹,再在内新建文件 query_page_info.go
  2. 在文件 query_page_info.go,该文件包名为service,然后导入包`import "web/repository"
  3. 写入以下代码
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层为关键。

  1. 在根目录新建controller文件夹,再在内新建query_page_info.go 文件(与service层的文件同名,你若改成其他文件名当然也是可以的)。

  2. 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,
        }
    }
    
  3. 新建测试文件 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,
        )
    }
    
  4. 测试方法不再赘述。在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的基础用法。

  1. 在根目录新建 server.go 文件,然后去看上面的Gin框架文档的入门指南

  2. 在根目录执行安装命令 go get -u github.com/gin-gonic/gin

  3. 把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 上启动服务
    }
    
  4. 导入repository包,书写数据初始化的代码

    import "web/repository"func main() {
        err := repository.InitTopicIndexMap("./data/")
        if err != nil {
            fmt.Println("初始化数据出错")
            os.Exit(-1)
        }
        /* ... */
    }
    
  5. 导入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端口上 启动服务
    
  6. 直接运行进行测试

  7. 在浏览器依次测试以下链接

到此,有关topic内容的查询服务 从底部到web服务 都搭建好了。

接下来完善有关post内容的查询服务。如果大家看不懂实践不顺利来催更我再补充后续内容。实现代码已经在github上有了。