0%

GoFrame 学习(1):快速开始

GoFrame 是一款模块化、高性能的 Go 语言开发框架,具有工程完备、开箱即用、高扩展性等特点,包含了常用的基础组件和开发工具,既可以作为完整的业务项目框架使用也可以作为独立的组件库使用。

快速开始

接下来我们将使用 GoFrame 来开发一个简单的 Web 服务,从而快速上手 GoFrame。如下代码使用 GoFrame 快速启动一个 Web Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)

func main() {
s := g.Server()
s.BindHandler("/", func(r *ghttp.Request) {
r.Response.Write("Hello World\n")
})
s.SetPort(8000)
s.Run()
}

接下来配置 go mod 并安装依赖:

1
2
# go mod init main
# go mod tidy

运行该程序:

1
2
3
4
5
6
7
8
# go run main.go
2025-06-28T15:07:00.962+08:00 [INFO] pid[2719616]: http server started listening on [:8000]
2025-06-28T15:07:00.962+08:00 [INFO] openapi specification is disabled

ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE
----------|--------|-------|-----------------|-------------
:8000 | ALL | / | main.main.func1 |
----------|--------|-------|-----------------|-------------

接下来使用 curl 访问该服务:

1
2
# curl 127.0.0.1:8000/
Hello World

快速解释一下上面代码:

  • g 组件是框架提供的一个耦合组件,封装和初始化一些常用的组件对象,例如 g.Server() 就可以获取一个默认的 Server 对象
  • 通过 Server 的 BindHandler 方法绑定路由及其处理函数,输出参数为当前请求对象 r *ghttp.Request,包含了当前请求的上下文信息
  • 通过调用 r.Response 对象的 Write 方法输出响应内容
  • 调用 Server 的 Run() 方法启动服务

获取请求参数

可以对上面的 Web Server 进行扩展,获取客户端提交的请求参数:

1
2
3
4
5
6
7
   ......
s.BindHandler("/", func(r *ghttp.Request) {
r.Response.Writef("Hello, %s, you are %d years old\n",
r.Get("name", "unknown").String(),
r.Get("age", 0).Int())
})
......

可以通过 r.Get() 方法来获取请求参数,它的第一个参数为参数名称,第二个参数为默认值,返回值是一个 gvar.Var 对象,它是一个运行时泛型对象,需要根据具体场景转换为特定类型值。

1
func (r *Request) Get(key string, def ...interface{}) *gvar.Var

r.Get() 可以获取所有 HTTP 请求方法提交的参数,例如它支持从 URL 查询参数、HTTP body 等中获取参数,而且可以自动识别不同的 Content-Type,例如 JSONx-www-form-urlencoded 等。

1
2
3
4
5
6
7
8
9
10
11
# curl 127.0.0.1:8000/
Hello, unknown, you are 0 years old

# curl '127.0.0.1:8000?name=jack&age=20'
Hello, jack, you are 20 years old

# curl 127.0.0.1:8000 -H "Content-Type:json" -d '{"name": "jane", "age": 25}'
Hello, jane, you are 25 years old

# curl 127.0.0.1:8000 -d 'name=mark&age=22'
Hello, mark, you are 22 years old

这种获取参数的方式有一些缺点,例如参数名称硬编码到代码中,而且与业务数据结构无法关联起来,可以通过结构化的参数对象来解决这个问题。

请求数据结构

通过 结构化数据 来提取请求参数,可以很好地维护参数的名称和类型。通过 r.Parse() 方法将请求参数映射到请求对象上,同样 r.Parse() 支持解析不同方法提交的请求参数:

1
2
3
4
5
6
7
8
9
10
11
12
   ......
s := g.Server()
s.BindHandler("/", func(r *ghttp.Request) {
var req HelloReq
if err := r.Parse(&req); err != nil {
r.Response.Write(err.Error())
return
}

r.Response.Writef("Hello, %s, you are %d years old\n", req.Name, req.Age)
})
.....
1
2
3
4
5
6
7
8
# curl '127.0.0.1:8000?name=jack&age=20'
Hello, jack, you are 20 years old

# curl 127.0.0.1:8000 -d 'name=mark&age=22'
Hello, mark, you are 22 years old

# curl 127.0.0.1:8000 -d '{"name": "jane", "age": 25}'
Hello, jane, you are 25 years old

使用规范路由

为了简化路由的注册方式,而且避免在每个路由处理函数中进行繁琐的参数解析,GoFrame 提供了 规范化的路由注册方式。为了使用 规范路由,首先需要定义请求数据结构和响应数据结构:

1
2
3
4
5
6
7
type HelloReq struct {
g.Meta `path:"/" method:"get"`
Name string `v:"required" dc:"name"`
Age int `v:"required" dc:"age"`
}

type HelloRes struct{}
  • 请求对象中新增了一个 g.Meta 对象,并提供过了一些结构体标签。该对象为元数据对象,用于给结构体嵌入一些标签信息:

    • path:路由的地址
    • method:HTTP 方法
  • 请求对象的其他属性中,也新增了一些标签:

    • v:valid 的缩写,用于自动校验参数
    • dc:description 的缩写,用于描述参数

当接口比较多时,手动配置路由与路由函数的关系有些繁琐,可以通过对象化的形式来封装路由函数。如下定义一个 路由对象

1
2
3
4
5
6
7
type Hello {}

func (h Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {
r := g.RequestFromCtx(ctx)
r.Response.Writef("Hello, %s, you are %d years old\n", req.Name, req.Age)
return
}
  • 路由对象用于封装路由函数,其所有定义的公开方法都将作为路由函数进行注册
  • 这里路由对象的 Say 方法其实就是一个路由函数,但是的它的定义更加符合业务逻辑函数的定义风格
  • 通过 g.RequestFromCtx 从 ctx 中获取原始的 ghttp.Request 对象,之后就可以返回自定义内容

最后通过如下方式完成路由的注册:

1
2
3
4
5
6
7
func main() {
......
s.Group("/", func(group *ghttp.RouterGroup) {
group.Bind(new(Hello))
})
......
}
  • 这里使用了 s.Group 来进行路由分组,在其回调方法中注册的所有路由,都会带有其定义的 分组路由前缀,这里即 /
  • 通过 group.Bind 方法注册路由对象,该方法将会遍历路由对象的所有公开方法,读取方法的输入输出结构体定义,并对其执行路由注册

代码运行如下:

1
2
3
4
5
# curl '127.0.0.1:8000?name=jack&age=20'
Hello, jack, you are 20 years old

# curl 127.0.0.1:8000 -H "Content-Type:json" -d 'name=mark&age=22'
Not Found

可以看到,此时已经不支持通过 POST 方法提交的请求了,因为我们的请求对象的 g.Meta 对象中指定了请求方法为 GET

中间件

中间件是一种拦截器设计,在 Web Server 中可以拦截请求/响应,并添加自定义处理逻辑。中间件的定义和普通的路由函数一样,但可以在 Request 参数中使用 Middleware 属性对象来控制请求流程。

中间件的类型分为两种:

  • 前置中间件:在路由服务函数调用之前
  • 后置中间件:在路由服务函数调用之后
1
2
3
4
5
func Middleware(r *ghttp.Request) {
// 前置中间件处理逻辑
r.Middleware.Next()
// 后置中间件处理逻辑
}

在中间件中执行完成处理逻辑后,使用 r.Middleware.Next() 方法进一步执行下一个流程。如果这个时候直接退出不调用 r.Middleware.Next() 方法的话,将会退出后续的执行流程(例如请求鉴权失败)。

如下使用中间件来实现错误处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ErrorHandler(r *ghttp.Request) {
r.Middleware.Next()
if err := r.GetError(); err != nil {
r.Response.Write("error occurs: ", err.Error())
return
}
}

func main() {
......
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ErrorHandler)
group.Bind(new(Hello))
})
......
}
  • 这里定义了 ErrorHandler 这个错误处理中间件,它首先调用 r.Middleware.Next() 执行路由函数流程,然后通过 r.GetError() 获取路由函数执行过程中产生的错误,如果有则进行处理。因此它是一个后置中间件
  • 在路由注册中,通过 group.Middleware(ErrorHandler) 给该分组下的所有路由,都绑定这个错误处理的中间件

再次运行,查看错误处理中间件的效果:

1
2
# curl 127.0.0.1:8000
error occurs: The Name field is required

统一返回结构

如果希望执行成功或者失败,都是以 json 格式返回应答,可以使用如下方法实现:

  • 首先定义路由函数返回的数据结构以及统一的数据结构
1
2
3
4
5
6
7
8
type HelloRes struct {
Content string `json:"content" dc:"result"`
}

type Response struct {
Message string `json:"message"`
Data interface{} `json:"data"`
}
  • 修改路由处理函数的实现,让它返回 HelloRes 数据结构,而不是直接写入响应内容
1
2
3
4
5
6
func (h Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {
res = &HelloRes{
Content: fmt.Sprintf("Hello, %s, you are %d years old", req.Name, req.Age),
}
return
}
  • 修改中间件的实现,统一返回 json 数据。这里的关键是通过 r.GetHandlerResponse() 获取路由函数返回的执行结果,并通过 r.Response.WriteJson 方法写入真正的响应内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func Middleware(r *ghttp.Request) {
r.Middleware.Next()

var (
msg string
res = r.GetHandlerResponse()
err = r.GetError()
)

if err != nil {
msg = err.Error()
} else {
msg = "OK"
}

r.Response.WriteJson(Response{
Message: msg,
Data: res,
})
}
  • 最后,路由注册相关代码如下所示:
1
2
3
4
5
6
   ......
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(Middleware)
group.Bind(new(Hello))
})
......

实际测试结果如下:

1
2
3
4
5
# curl '127.0.0.1:8000?name=jack&age=20'
{"message":"OK","data":{"content":"Hello, jack, you are 20 years old"}}

# curl 127.0.0.1:8000
{"message":"The Name field is required","data":null}

通过中间件对返回的数据统一进行封装,这对有着大量 API 接口的业务来说是很有必要的

自动化生成接口文档

使用 GoFrame 框架自动化生成接口文档非常简单:

1
2
3
4
5
6
7
func main() {
......
s.SetOpenApiPath("/api.json")
s.SetSwaggerPath("/swagger")
s.SetPort(8000)
s.Run()
}
  • s.SetOpenApiPath("/api.json") 启用 OpenAPIv3 接口文档生成,访问路径设置为 /api.jsonOpenAPIv3 是目前业界接口文档的标准协议,通常使用 json 格式生成,这个 json 文件可以使用很多接口管理攻击打开,例如 Swagger UIPostman

  • s.SetSwaggerPath("/swagger") 启用内置的接口文档 UI 工具,访问路径设置为 /swaggerswagger 是常用的接口文档 UI 工具,支持多种接口文档格式,例如 OpenAPIv3。其实准确地说,GoFrame 框架内置的 UI 工具是 redoc,而不是 swagger ui

另外,我们可以按照 OpenAPIv3 接口协议规范,在 g.Meta 中继续完善接口定义,例如添加如下标签:

  • tags:接口分类/模块
  • summary:接口描述

现在我们访问对应的 URI 地址,就能看到接口文档了,也能使用 Swagger UI 查看接口:

项目脚手架

GoFrame 框架同时还提供了 项目脚手架 工具,以快速生成标准化、工程化的项目框架代码。

安装框架工具

如下在 Linux 系统上下载并安装预编译的框架工具:

1
wget -O gf "https://github.com/gogf/gf/releases/latest/download/gf_$(go env GOOS)_$(go env GOARCH)" && chmod +x gf && ./gf install -y && rm ./gf

确认安装成功:

1
2
3
4
5
6
7
# gf -v
v2.9.0
Welcome to GoFrame!
Env Detail:
Go Version: go1.23.1 linux/amd64
GF Version(go.mod): cannot find go.mod
......

创建项目模版

使用如下命令快速创建一个工程项目,项目名称为 demo,-u 参数用于指定是否更新项目中使用的 GoFrame 框架:

1
# gf init demo -u

生成的项目脚手架是按照通用性设计的,可以满足 Web、微服务等开发场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cd demo
# tree -L 1 --dirsfirst
.
├── api
├── hack
├── internal
├── manifest
├── resource
├── utility
├── go.mod
├── go.sum
├── main.go
├── Makefile
└── README.MD

默认会生成一个 HTTP Web Server 的模版项目,因此直接可以直接运行该项目:

1
2
3
4
5
# go run main.go
2025-06-30T13:21:36.393+08:00 [INFO] pid[3830415]: http server started listening on [:8000]
2025-06-30T13:21:36.393+08:00 [INFO] swagger ui is serving at address: http://127.0.0.1:8000/swagger/
2025-06-30T13:21:36.393+08:00 [INFO] openapi specification is serving at address: http://127.0.0.1:8000/api.json
.....
1
2
# curl 127.0.0.1:8000/hello
Hello World!

如果想要更新项目使用的 GoFrame 框架版本,可以在项目根目录下(目录下有 go.mod 文件)使用如下命令:

1
gf up -a

项目启动分析

接下来将对该工具生成的 脚手架 项目的启动流程进行分析,以熟悉工具生成的 脚手架 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
_ "demo/internal/packed"

"github.com/gogf/gf/v2/os/gctx"

"demo/internal/cmd"
)

func main() {
cmd.Main.Run(gctx.GetInitCtx())
}
  • 程序入口由 main.go 进入,该文件调用 internal/cmd 包的 Main.Run 启动程序
  • 项目的所有核心业务逻辑都放到了 internal 目录下,以封装内部实现
  • 框架的核心组件均需要传递 context 上下文参数,这里使用 gctx.GetInitCtx() 获取一个 Context

Main.Run 函数主要完成启动逻辑,默认它会启动一个 HTTP Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
hello.NewV1(),
)
})
s.Run()
return nil
},
}
)
  • 这里仍然是通过 s.Group() 的方式创建分组路由,在路由处理函数内,通过 Middleware 方法添加中间件 ghttp.MiddlewareHandlerResponse,用于规范 HTTP 响应
  • 通过 Bind 方法绑定 hello.NewV1() 所返回的路由对象,这个路由对象下的所有公开方法均会自动注册到路由中。项目脚手架支持接口的版本管理,默认返回的路由对象都是 v1 版本

NewV1() 的返回值其实是一个接口,而不是具体的对象。这样实现的理由是:当我们定义了很多 API 接口,但是具体实现的 controller 对象可能只实现了其中的一部分,为了尽早发现这个问题,我们就可以使用 Go 的接口特性了。假设具体实现(例如这里的 ControllerV1)只实现了接口的部分方法,那么在编译时就会报错,就不用等到运行时才能发现这个问题了

1
2
3
4
5
6
7
8
9
10
11
12
type IHelloV1 interface {
Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error)
}

func NewV1() hello.IHelloV1 {
return &ControllerV1{}
}

func (c *ControllerV1) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) {
g.RequestFromCtx(ctx).Response.Writeln("Hello World!")
return
}

从具体的 路由函数实现 中可以看到,请求参数的定义是 v1.HelloReq,响应参数的定义是 v1.HelloRes,从这两个参数的定义中可以看到路由信息:

1
2
3
4
5
6
7
type HelloReq struct {
g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"`
}

type HelloRes struct {
g.Meta `mime:"text/html" example:"string"`
}

最后通过调用 ghttp.Server.Run() 方法启动 HTTP Server,开始接收 HTTP 请求。

接口开发

接下来继续在脚手架框架代码中编写简单的 CRUD 接口,实现对数据库表的增删改查操作。

设计数据表

在接口开发之前先设计数据库表是比较好的开发习惯,如下流程启动 mysql 数据库并创建数据库表:

  • 首先启动 mysql 数据库:
1
2
3
4
5
docker run -d --name mysql \
-p 3306:3306 \
-e MYSQL_DATABASE=test \
-e MYSQL_ROOT_PASSWORD=12345678 \
mysql:8.0
  • 测试数据库连接成功
1
docker exec -it mysql mysql -h 127.0.0.1 -P 3306 -u root -p12345678 test
  • 使用如下命令创建数据表:
1
2
3
4
5
6
7
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'user id',
`name` varchar(45) DEFAULT NULL COMMENT 'user name',
`status` tinyint DEFAULT NULL COMMENT 'user status',
`age` tinyint unsigned DEFAULT NULL COMMENT 'user age',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
6
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| user |
+----------------+

生成 dao/do/entity

开发工具的配置在 hack/config.yaml 文件中维护,我们首先需要检查生成的默认配置是否符合预期:

1
2
3
4
5
6
7
8
9
10
gfcli:
gen:
dao:
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
descriptionTag: true

docker:
build: "-a amd64 -s linux -p temp -ew"
tagPrefixes:
- my.image.pub/my-app

当我们执行 make dao 命令时,就会用到该配置文件中的 dao 部分配置:

1
2
3
4
5
6
# make dao
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/dao/user.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/dao/internal/user.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/model/do/user.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/model/entity/user.go
done!

执行 make dao 命令后,会自动生成 dao/do/entity 文件。每张表将会生成 3 类 Go 文件:

  • dao:通过对象方式访问底层数据源,底层基于 ORM 组件实现
  • do:数据转换模型,用于业务模型到数据模型的转换,由工具维护,用户不能修改
  • entity:数据模型,由工具维护,用户不能修改

对于工具生成的代码,如果有 Code generated and maintained by GoFrame CLI tool. DO NOT EDIT 的注释,提示我们不要手动修改这些文件。

生成的 dao 文件有 2 个:

  • internal/dao/internal/user.go 用于封装对数据表 user 的访问,提供一些数据结构和方法以简化对数据表的 CRUD 操作
  • internal/dao/user.gointernal/dao/internal/user.go 的进一步封装,用于供其他模块直接调用访问。开发者可以按需修改该文件

internal/dao/user.go 提供的方法与对象才是可供其他模块调用的接口,它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package dao

import (
"demo/internal/dao/internal"
)

// userDao is the data access object for the table user.
// You can define custom methods on it to extend its functionality as needed.
type userDao struct {
*internal.UserDao
}

var (
// User is a globally accessible object for table user operations.
User = userDao{internal.NewUserDao()}
)

// Add your custom methods and functionality below.

生成的 do 代码文件 internal/model/do/user.go 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package do

import (
"github.com/gogf/gf/v2/frame/g"
)

// User is the golang structure of table user for DAO operations like Where/Data.
type User struct {
g.Meta `orm:"table:user, do:true"`
Id interface{} // user id
Name interface{} // user name
Status interface{} // user status
Age interface{} // user age
}

生成的 entity 代码文件 internal/model/entity/user.go 内容如下:

1
2
3
4
5
6
7
8
9
package entity

// User is the golang structure for table user.
type User struct {
Id uint `json:"id" orm:"id" description:"user id"` // user id
Name string `json:"name" orm:"name" description:"user name"` // user name
Status int `json:"status" orm:"status" description:"user status"` // user status
Age uint `json:"age" orm:"age" description:"user age"` // user age
}

可以看到,entity 数据结构定义与数据表字段一一对应。

编写 API 接口定义

api 子目录下,定义 CRUD 接口,接口采用 RESTful 风格,充分使用 GET/POST/PUT/DELETE 的 HTTP Method,同时使用 v1 作为版本号。API 接口代码文件 api/user/v1/user.go 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package v1

import (
"demo/internal/model/entity"

"github.com/gogf/gf/v2/frame/g"
)

// Status marks user status.
type Status int

const (
StatusOK Status = 0 // User is OK.
StatusDisabled Status = 1 // User is disabled.
)

type CreateReq struct {
g.Meta `path:"/user" method:"post" tags:"User" summary:"Create user"`
Name string `v:"required|length:3,10" dc:"user name"`
Age uint `v:"required|between:18,200" dc:"user age"`
}
type CreateRes struct {
Id int64 `json:"id" dc:"user id"`
}

type UpdateReq struct {
g.Meta `path:"/user/{id}" method:"put" tags:"User" summary:"Update user"`
Id int64 `v:"required" dc:"user id"`
Name *string `v:"length:3,10" dc:"user name"`
Age *uint `v:"between:18,200" dc:"user age"`
Status *Status `v:"in:0,1" dc:"user status"`
}
type UpdateRes struct{}

type DeleteReq struct {
g.Meta `path:"/user/{id}" method:"delete" tags:"User" summary:"Delete user"`
Id int64 `v:"required" dc:"user id"`
}
type DeleteRes struct{}

type GetOneReq struct {
g.Meta `path:"/user/{id}" method:"get" tags:"User" summary:"Get one user"`
Id int64 `v:"required" dc:"user id"`
}
type GetOneRes struct {
*entity.User `dc:"user"`
}

type GetListReq struct {
g.Meta `path:"/user" method:"get" tags:"User" summary:"Get users"`
Age *uint `v:"between:18,200" dc:"user age"`
Status *Status `v:"in:0,1" dc:"user age"`
}
type GetListRes struct {
List []*entity.User `json:"list" dc:"user list"`
}
  • 这里实现了 User 的创建、更新、删除和查询接口(单个查询和列表查询)
  • 在请求对象的定义中,同样使用了 g.Meta 来管理接口的元数据信息
  • 在请求对象的其他属性中,使用了 v 标签来进行参数校验,requiredlengthbetween 等都是内置的校验规则
  • 在响应对象中,通过 json 标签来定义 json 序列化时的字段名
  • 对于 path 标签 /user/{id},其中 {id} 表示一个路由参数。该参数通过 URL Path 的方式传递,参数名称为 id。从路由中匹配到的 id 参数会自动赋值给请求对象的同名属性(不区分大小写)
  • UpdateReq 请求对象中,我们看到一些参数使用了指针类型。这是避免类型默认值对接口的影响,例如如果 Status 字段不使用指针,则它的默认值就是 0,那么就无法区分调用端到底有没有传递该参数。而使用指针(其默认值是 nil)则可以很好地进行区分
  • 在查询接口中,返回的数据直接使用了 *entity.User 结构体

这种接口定义方式可以自动化地生成接口文档,保证文档与代码的一致性(代码即文档)。

根据 api 生成代码

当 api 定义完成后,直接通过 make ctrl 命令(或者 gf gen ctrl)生成 controller 代码:

1
2
3
4
5
6
7
8
9
10
# make ctrl
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/api/hello/hello.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/api/user/user.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/controller/user/user.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/controller/user/user_new.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/controller/user/user_v1_create.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/controller/user/user_v1_update.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/controller/user/user_v1_delete.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/controller/user/user_v1_get_one.go
generated: /root/code/private/go/go_open/gofg_learn/gf_tool/demo/internal/controller/user/user_v1_get_list.go

生成的代码主要包含 3 类文件:

  • api 接口抽象文件:例如 api/user/user.go 文件就是对 API 接口的 interface 定义,这样能在编译期就发现某些接口未实现的问题
1
2
3
4
5
6
7
type IUserV1 interface {
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error)
Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error)
GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error)
GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error)
}
  • controller 路由对象管理:用于管理 controller 的初始化
  • internal/controller/user/user.go:其实是一个空文件,可用于定义一些 controller 内部使用的数据结构、常量等
  • internal/controller/user/user_new.go:包含路由对象创建函数
1
2
3
4
5
6
7
8
9
10
11
package user

import (
"demo/api/user"
)

type ControllerV1 struct{}

func NewV1() user.IUserV1 {
return &ControllerV1{}
}
  • controller 路由实现代码:用于实现 API 接口,默认会按照一个 api 接口一个源文件的形式生成代码(也可以控制聚合到一个源码文件中)
    • internal/controller/user/user_v1_create.go
    • internal/controller/user/user_v1_delete.go
    • internal/controller/user/user_v1_get_list.go
    • internal/controller/user/user_v1_get_one.go
    • internal/controller/user/user_v1_update.go

默认生成的代码内容如下所示,需要我们自己完善对应的业务逻辑:

1
2
3
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
return nil, gerror.NewCode(gcode.CodeNotImplemented)
}

完成接口逻辑实现

通过脚手架工具,很多与业务逻辑无关的代码都已经生成好了,我们只需要在对应的代码文件中实现业务逻辑即可。使用 GoFrame 框架数据库 ORM 组件,可以非常方便、高效完成接口开发工作。

例如修改 internal/controller/user/user_v1_create.go 文件,实现用户创建逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
id, err := dao.User.Ctx(ctx).Data(do.User{
Name: req.Name,
Status: v1.StatusOK,
Age: req.Age,
}).InsertAndGetId()
if err != nil {
return nil, err
}
res = &v1.CreateRes{
Id: id,
}
return
}
  • 通过 dao.User 通过 dao 组件操作 user 表
  • 每个 dao 操作都需要传递 ctx 参数,通过 Ctx(ctx) 方法创建一个 gdb.Model 对象,该对象是框架的模型对象,用于操作特定的数据表
  • 通过 Data 方法传递需要写入数据表的数据,使用 do 转换模型对象 输入我们的数据,由 do 模型在底层自动转换为对应的数据表字段类型
  • 最后通过 InsertAndGetId 方法执行插入操作,并返回新创建数据的 ID

另外参数的校验逻辑不需要在 Controller 中编写,因为我们在定义请求对象时已经通过 v 标签定义好了,框架会自动进行校验,如果校验失败,不会执行对应的路由函数。因此只需要在路由函数中实现业务逻辑即可。

如下分别实现剩余几个接口:

1
2
3
4
func (c *ControllerV1) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) {
_, err = dao.User.Ctx(ctx).WherePri(req.Id).Delete()
return
}
1
2
3
4
5
6
7
8
func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
_, err = dao.User.Ctx(ctx).Data(do.User{
Name: req.Name,
Status: req.Status,
Age: req.Age,
}).WherePri(req.Id).Update()
return
}
1
2
3
4
5
6
7
8
func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) {
res = &v1.GetListRes{}
err = dao.User.Ctx(ctx).Where(do.User{
Age: req.Age,
Status: req.Status,
}).Scan(&res.List)
return
}
1
2
3
4
5
func (c *ControllerV1) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) {
res = &v1.GetOneRes{}
err = dao.User.Ctx(ctx).WherePri(req.Id).Scan(&res.User)
return
}

配置与路由

GoFrame 的数据库组件使用了接口化设计,接口与实现是分离的,以提供更好的抽象型和扩展性。由于当前我们使用 mysql 数据库,因此需要引入 mysql 驱动,在 `main.go 中新增如下代码即可:

1
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"

之后需要完成项目配置,脚手架生成的文件中主要有两个配置文件:

  • hack/config.yaml:用于配置脚手架工具本身,主要在项目开发过程中使用
  • manifest/config/config.yaml:业务项目的配置文件,由开发者自行维护。默认生成的配置内容如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# https://goframe.org/docs/web/server-config-file-template
server:
address: ":8000"
openapiPath: "/api.json"
swaggerPath: "/swagger"

# https://goframe.org/docs/core/glog-config
logger:
level : "all"
stdout: true

# https://goframe.org/docs/core/gdb-config-file
database:
default:
link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"

最后,我们还需要为新添加的接口注册路由,修改 internal/cmd/cmd.go 文件,在 group.Bind 方法中,通过 user.NewV1() 添加我们的路由对象即可。

运行项目

  • 通过如下命令启动服务:
1
2
3
4
5
# go run main.go
2025-07-02T14:07:54.560+08:00 [INFO] pid[802037]: http server started listening on [:8000]
2025-07-02T14:07:54.560+08:00 [INFO] swagger ui is serving at address: http://127.0.0.1:8000/swagger/
2025-07-02T14:07:54.560+08:00 [INFO] openapi specification is serving at address: http://127.0.0.1:8000/api.json
......
  • 我们可以访问对应的 http://127.0.0.1:8000/swagger/ 来使用 Swagger UI 查看 API 地址

  • 如下使用 curl 测试接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#  curl -X POST 'http://127.0.0.1:8000/user' -d '{"name":"john","age":20}'
{"code":0,"message":"OK","data":{"id":1}}

# curl 'http://127.0.0.1:8000/user'
{"code":0,"message":"OK","data":{"list":[{"id":1,"name":"john","status":0,"age":20}]}}

# curl 'http://127.0.0.1:8000/user/1'
{"code":0,"message":"OK","data":{"id":1,"name":"john","status":0,"age":20}}

# curl -X PUT 'http://127.0.0.1:8000/user/1' -d '{"name":"john","age":30}'
{"code":0,"message":"OK","data":null}

# curl -X DELETE 'http://127.0.0.1:8000/user/1'
{"code":0,"message":"OK","data":null}

# curl 'http://127.0.0.1:8000/user'
{"code":0,"message":"OK","data":{"list":null}}

小结

可以看到,使用 GoFrame 脚手架工具来进行 CRUD 接口的开发,主要的几件事情是:

  • 数据库表涉及
  • api 接口定义
  • 接口的业务逻辑实现
  • 简单的配置、路由注册

总结

这篇文章学习了 GoFrame 框架的基本使用方法,可以看到,使用 GoFrame 框架来开发项目的确比较高效,而且 GoFrame 框架的文档非常丰富,解释也很详尽,的确是一个非常值得学习的开源项目。

Reference