文章目录
- 简介
- 开始!
- 建立数据库,设计表
- [后端 go-frame 脚手架](#后端 go-frame 脚手架)
-
- [生成 dao、do、entity](#生成 dao、do、entity)
- 编写接口定义CRUD
- [生成 controller 代码](#生成 controller 代码)
- 完成接口逻辑实现
- 配置与路由
- 启动和测试接口
- 总结
简介
个人写前端比较多,来学习一个 go-frame 框架
后端并不熟练,所以没有很细致的步骤,所以写作的顺序是偏前端思考,属于是想到哪儿就写到哪里了,更像是流水账
我的目标:做一个非常小的项目 todo-list
想了解并练习以下内容:
RESTFul架构go-frame刚接触的学习,包括自动生成的命令, 根据 api 生成代码- 思考:实现一个功能 => 设计一个接口,从哪儿开始入手,我能想到的,大概就是:先分析这个接口的需求是什么,应该有哪些字段,接着定义表结构,之后实现增删改查等操作...
技术栈:前端采用 React19 ,或者 Vue3 ,随便,后端用 go-frame,数据库 MySql,(本地的,官方教程用的是 docker 的 mysql,都可以,用本地的话,我觉得可以用一些可视化的软件比方说 navcat,比较方便而已)
开始!
先从后端数据库字段的设计开始
建立数据库,设计表
如果想设计一个软件,就要想有哪些功能,对于 todo 这样的小项目,功能也就如下几个
- todo 的名称
- todo 的状态:是否做完
- 创建时间 & 更新时间
- 先实现最简单的, 核心功能,之后有更复杂的功能再另说,因为可能要加新的字段之类的操作
好,有了功能之后,应该能映射出一些需要的字段了,也就是数据模型
根据上面的功能,我觉的应该有的字段: id, title, done, created_at,updated_at, ,然后建一个表
然后发现,我还没有数据库,那先创建一个数据库
连接 mysql,输入密码
bash
mysql -u root -p

然后创建一个数据库,发现我之前已经有 todo_app 的数据库了,那就创建一个叫 todo_app_go_frame 的数据库
sql
CREATE DATABASE todo_app_go_frame CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

切换数据库
sql
USE todo_app_go_frame;

然后创建一个表
sql
CREATE TABLE todo (
id INT AUTO_INCREMENT PRIMARY KEY, -- 主键,自增,用来唯一标识一条 todo
title VARCHAR(255) NOT NULL, -- 任务标题,最长 255 字符,必填
done TINYINT(1) NOT NULL DEFAULT 0, -- 是否完成:0 未完成,1 完成;默认未完成 数据库中并没有布尔类型,所以用TINYINT 最小整数来判断
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间,插入时自动填充
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 更新时间,更新时自动刷新
);

其中的创建时间和更新时间的解释如下图。

此时,表应该创建好了,用下面的命令查看一下
sql
SHOW TABLES;
DESCRIBE todo;

至此,数据库,表结构算是建立完成了,接下来用 go-frame 脚手架把后端搭起来
后端 go-frame 脚手架
首先通过脚手架,搭建出一个空的壳子出来
这里用的是
gf命令,就是 go-frame 的命令
bash
gf init 项目名称 -u
gf init back-end -u

跑完之后,大概长这个样子,工程目录设计的话,官方也有解释,这里就不赘述了

此时运行 go run main.go 就能跑起来项目了!

生成 dao、do、entity

好的,果不其然报错了,来改一下数据库的名称,之后再试一下


哦!这次成功了,可以看到他生成了 4 个文件
其中的三个文件都有 Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. 的标记,说明他是自动生成的,每次生成都会被覆盖掉的

对于这个命令,官方的解释是

看这个样子,我只需要修改一个文件就够了,官方文档是这样说的:


ok,接着我们来写 crud 了!
编写接口定义CRUD
我还是结合官方文档的代码设计风格,gpt 来辅助
我们在 api 文件夹下,新增 todo 文件夹,然后新增 v1 文件夹
同样的,我们默认开始使用v1版本。使用版本号做为良好的开发习惯,有利于未来接口的兼容性维护。
尝试写了一个,感觉写这个也挺麻烦的。

接口定义中,使用
g.Meta来管理接口的元数据信息,这些元数据信息通过标签的形式定义在g.Meta属性上。这里的元数据信息包括:path路由地址、method请求方式、tags接口分组(用于生成接口文档)、summary接口描述。这些元数据信息都是OpenAPIv3里面的东西,我们这里不做详细介绍,大家了解即可
原文链接
原文:只有返回的参数结构体中带有 json 标签,因为返回的数据往往需要转换为 json 格式给前端使用,通过 snake 的参数命名的方式更符合前端命名习惯这个 snake 命名是什么?
"snake 命名"就是 蛇形命名法(snake_case),在后端开发中非常常见
其他的接口,我就复制过来改吧改吧好了
好的,增加和删除都好做,来到了更新,那么就有 done 这个字段,我该如何定义呢?
看了一下官网的代码,这里有个 status 的状态似乎很符合 done 字段的逻辑,所以我照葫芦画瓢试一下。

这里为什么要用指针呢?gpt 给的例子很好理解


查询的方法也是照葫芦画瓢写的
go
// 查询单个接口
type GetOneReq struct {
g.Meta `path:"/todo/{id}" method:"get" tags:"todo" summary:"Get one todo"`
Id int64 `v:"required" dc:"todo id"`
}
type GetOneRes struct {
*entity.Todo `dc:"todo"`
// 这里的返回结果我们使用了*entity.User结构体,该结构是前面我们通过make dao命令生成的entity,该数据结构与数据表字段一一对应。
}
// 查询多个接口,加上了查询
type GetListReq struct {
g.Meta `path:"/todo" method:"get" tags:"Todo" summary:"Get todo"`
Title *string `v:"length:1,10" dc:"todo title"`
Done *Status `v:"in:0,1" dc:"todo done"`
}
type GetListRes struct {
List []*entity.Todo `json:"list" dc:"todo list"`
}
生成 controller 代码
又一个爽的地方来了,没错就是代码生成!
写好 api 之后,可以通过命令 make ctrl 生成基础的代码

不过增删改查,只是个模板,具体的实现需要我们接着手写。
完成接口逻辑实现
好,开始写创建接口,照葫芦画瓢
go
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
insertId, err := dao.Todo.Ctx(ctx).Data(do.Todo{
Title: req.Title,
}).InsertAndGetId()
if err != nil {
return nil, err
}
res = &v1.CreateRes{
Id: insertId,
}
return
}
ok,每行拆开来学习一下
在
Create实现方法中:
- 我们通过dao.User通过dao组件操作user表。
- 每个
dao操作都需要传递ctx参数,因此我们通过Ctx(ctx)方法创建一个gdb.Model对象,该对象是框架的模型对象,用于操作特定的数据表。- 通过
Data传递需要写入数据表的数据,我们这里使用do转换模型对象输入我们的数据。do转换模型会自动过滤nil数据,并在底层自动转换为对应的数据表字段类型。- 在绝大部分时候,我们都使用
do转换模型来给数据库操作对象传递写入/更新参数、查询条件等数据。
通过 InsertAndGetId 方法将 Data 的参数写入数据库,并返回新创建的记录主键 id。

接着是强制错误处理
然后是我比较疑惑的地方,为什么 res 用的是指针呢?
- 首先,代码生成的模板中,函数签名,返回的
res就是指针

ok 当我写到删除的时候,又有了新的问题


还有一个 go 语法的问题,gpt 是这样回答我的

再来写更新接口,这个更新接口好像上面的增加和删除的写法的结合
首先创建数据结构,然后根据 id ,找到表中的数据,然后调用 update 更新数据(当然都是我猜的)
go
func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
_, err = dao.Todo.Ctx(ctx).Data(do.Todo{
Title: req.Title,
Done: req.Done,
}).WherePri(req.Id).Update()
return
}
然后来实现查找吧,查找分为单条查找和多条查找
单条查找的话,我按照我前面所学的内容,憋了一条出来,很明显不对,

好吧,方法都用错了,是 wherePri,看了眼官方的例子:
go
func (c *ControllerV1) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) {
res = &v1.GetOneRes{}
err = dao.Todo.Ctx(ctx).WherePri(req.Id).Scan(&res.Todo)
return
}
我让 gpt 来解释了一下这个代码

非常困惑这个 scan 的方法,然后我问 gpt


嗯,gpt 回答的还算是我能理解的,又学到个新的 scan() 方法,不过应该是框架封装的。
然后实现返回列表的,有了查询单个的学习,那这个查询多个的,只是多加了 where 的条件,where 就是数据库的条件查询了,这个好理解,scan 也好理解。
go
func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) {
res = &v1.GetListRes{}
err = dao.Todo.Ctx(ctx).Where(do.Todo{
Title: req.Title,
Done: req.Done,
}).Scan(&res.List)
return
}
至此,所有的接口都实现的差不多啦!但是我还没测试,所以可能会有问题!
配置与路由
好,官网上让我们首先引入数据库驱动,我先不添加数据驱动,因为作为正常的新手,我现在更急迫的想测试一下接口写的咋样了?
所以先注册一下路由吧,然后重启一下项目

可以看到这些路由已经注册好了,现在开始访问一下!
哦吼,果然报错了,不错不错,好事情


问题:所以,我理解的是,在这个 go 项目启动的时候,是不会再启动的时候去链接数据库,只有在访问资源的时候,后端发现需要链接数据库的时候才去访问数据库?
gpt 给的答案:

好的,又学到了,延迟链接
ok,现在我来配好驱动和数据库的密码,加入驱动爆红

说明没安装驱动啊老弟,安装一下,有魔法的开魔法
bash
go get github.com/gogf/gf/contrib/drivers/mysql/v2
安装完之后就好了,不飘红了

如果只是安装驱动,没做配置项的话,报这个错误

来改一下密码

启动和测试接口
重启项目

可以了,那我们试着加一个数据进去
bash
curl -X POST 'http://127.0.0.1:8000/todo' -d '{"title":"测试我的 go-frame 的新增接口"}'
之后返回的数据如下,看来是触碰到了那个校验哈

看一下他的返回,原来如此,是 title 太 tm 长了。那我想把校验改的长一点,合理吧?

然后重启一下后端,再试一下
bash
curl -X POST 'http://127.0.0.1:8000/todo' -d '{"title":"测试我的 go-frame 的新增接口"}'

ok 啊,终于添加进去了,用 navcat 看一下表里,也成功添加进去了!

拿浏览器来请求一下,不错也有数据了!

再来试试修改!
bash
curl -X PUT 'http://127.0.0.1:8000/todo/1' -d '{"done":1}'


不错,也好使,再来试试删除!
bash
curl -X DELETE 'http://127.0.0.1:8000/todo/1'


总结
后端,基本的操作已经过了一遍
在这个过程中,产生了一些疑问
- todo 表,如果新增了字段,怎么处理?要不要再泡一下代码生成?
- 什么场景需要创建一个新表?
- 什么时候需要创建新的数据库?
- 目前接口用的都是
v1的版本,我如何用v2,怎么切换?
这些问题我都挺好奇的,不过就先到这里,下次把前端的东西搭起来
