GoFrame框架学习笔记
朋友,你来晚了,不过没关系,我们的故事从现在开始,我们准备了一些使用指南,助你快速了解 GoFram
https://goframe.org/
快速开始
go版本限制
为保证框架的稳定性和安全性,GoFrame框架要求的最低的基础Go语言版本通常会比最新的Go语言版本低1~3个版本。
当前最新框架版本要求的最低Go语言版本:
golang版本 >= 1.20
安装
初始化go.mod:
go mod init hello
下载框架最新版本:
go get -u -v github.com/gogf/gf/v2
运行
我们尝试运行以下代码:
main.go
package mainimport ("fmt""github.com/gogf/gf/v2"
)func main() {fmt.Println("Hello GoFrame:", gf.VERSION)
}
Hello World
新建main.go文件
main.go
package mainimport ("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!")})s.SetPort(8000)s.Run()
}
配置go mod并安装依赖
go mod init main
go mod tidy
- 任何时候,您都可以通过 g.Server() 方法获得一个默认的 Server 对象,该方法采用单例模式设计, 也就是说,多次调用该方法,返回的是同一个 Server 对象。其中的g组件是框架提供的一个耦合组件,封装和初始化一些常用的组件对象,为业务项目提供便捷化的使用方式。
- 通过Server对象的BindHandler方法绑定路由以及路由函数。在本示例中,我们绑定了/路由,并指定路由函数返回Hello World。
- 在路由函数中,输入参数为当前请求对象r *ghttp.Request,该对象包含当前请求的上下文信息。在本示例中,我们通过r.Response返回对象直接Write返回结果信息。
通过SetPort方法设置当前Server监听端口。在本示例中,我们监听8000端口,如果在没有设置端口的情况下,它默认会监听一个随机的端口。 - 通过 Run() 方法阻塞执行 Server 的监听运行。
获取请求
直接获取请求参数
package mainimport ("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.Writef("Hello %s! Your Age is %d",r.Get("name", "unknown").String(),r.Get("age").Int(),)})s.SetPort(8000)s.Run()
}
在GoFrame框架中,获取参数非常便捷。在本示例中,我们通过r.Get方法获取客户端提交的参数,该方法能够获取所有HTTP Method提交的参数, 比如Query String/Form/Body等,其内部将会根据客户端提交的类型自动识别解析,比如支持自动识别参数格式例如json/xml等。该方法的定义如下:
func (r *Request) Get(key string, def ...interface{}) *gvar.Var
可以看到,Get方法接受两个参数,第一个为参数名称,第二个参数为非必须参数,表示默认值。返回结果为一个*gvar.Var对象,该对象为GoFrame框架 提供的运行时泛型对象,开发者可以根据业务场景需要将参数转换为各种类型。
绑定结构参数与校验
我们定义一个请求的数据结构来接收客户端提交的参数信息:
type HelloReq struct {
Name string // 名称
Age int // 年龄
}
太棒了,看起来我们既可以对参数进行注释描述,也能确定参数的类型,不再需要参数名称硬编码。
package mainimport ("github.com/gogf/gf/v2/frame/g""github.com/gogf/gf/v2/net/ghttp"
)type HelloReq struct {Name string // 名称Age int // 年龄
}func main() {s := g.Server()s.BindHandler("/", func(r *ghttp.Request) {var req HelloReqif err := r.Parse(&req); err != nil {r.Response.Write(err.Error())return}if req.Name == "" {r.Response.Write("name should not be empty")return}if req.Age <= 0 {r.Response.Write("invalid age value")return}r.Response.Writef("Hello %s! Your Age is %d",req.Name,req.Age,)})s.SetPort(8000)s.Run()
}
parse方法没区分get与post
在本示例中:
我们通过r.Parse方法将请求参数映射到请求对象上,随后可以通过对象的方式来使用参数。 r.Parse方法支持自动解析客户端提交参数,并赋值到指定对象上。 内部有固定的名称映射逻辑,您将在开发手册的类型转换组件中详细了解到,这里不作过多介绍。
同时,我们在本示例中增加了校验逻辑,Name及Age参数不能为空。
规范路由——便捷操作
为了简化路由注册方式,避免一些繁琐的参数处理细节, 让开发者将精力聚焦于业务逻辑本身,GoFrame框架提供了规范化的路由注册方式。 规范化的路由注册方式,我们为了见名知意,便命名为了规范路由。
数据结构定义
在规范路由中,我们同样定义一个请求的数据结构来接收客户端提交的参数信息,但同时需要定义一个返回对象。 目的是为了未来返回参数扩展的需要,以及未来标准化接口文档生成的需要。
type HelloReq struct {g.Meta `path:"/" method:"get"`Name string `v:"required" dc:"姓名"`Age int `v:"required" dc:"年龄"`
}
type HelloRes struct {}
简要介绍:
在请求对象中,我们多了一个g.Meta对象的引用,并给定了一些结构体标签。该对象为元数据对象,用于给结构体嵌入 一些定义的标签信息。例如在本示例中:
- path:表示注册的路由地址。
- method:表示注册绑定的HTTP Method。
在属性中同样出现两个新的标签名称:
v:表示校验规则,为valid的缩写,用于自动校验该参数。这里使用v:"required"表示该参数为必需参数,如果客户端未传递该参数时,服务端将会校验失败。
dc:表示参数描述信息,为description的缩写,用于描述该参数的含义。
信息
在开发手册的对应章节中,有关于全部标签信息以及校验组件的详细讲解,这里只需要了解其作用即可,不做过多介绍。
路由对象管理
为了更好地管理路由注册,特别是接口比较多的场景下,如果手动一一去配置路由与路由函数关系太过于繁琐。 我们通过对象化的形式来封装路由函数,通过对象化封装的方式来简化我们的路由管理。 我们定义一个路由对象如下:
type Hello struct{}func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {r := g.RequestFromCtx(ctx)r.Response.Writef("Hello %s! Your Age is %d",req.Name,req.Age,)return
}
发现多了一个req的参数
我们定义了一个Hello对象,该对象用于封装路由函数,其所有定义的公开方法都将被作为路由函数进行注册。
可以看到该路由对象的Say方法的路由函数的定义方式,相比较于func(ghttp.Request)的路由函数定义方式,更符合业务逻辑函数的定义风格。
在路由回调方法Say中,我们通过g.RequestFromCtx方法从ctx获取原始的ghttp.Request请求对象,用于自定义返回内容数据。
完整示例代码
我们调整我们前面的Web Server程序如下:
main.go
package mainimport ("context""github.com/gogf/gf/v2/frame/g""github.com/gogf/gf/v2/net/ghttp"
)type HelloReq struct {g.Meta `path:"/" method:"get"`Name string `v:"required" dc:"姓名"`Age int `v:"required" dc:"年龄"`
}
type HelloRes struct{}type Hello struct{}func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {r := g.RequestFromCtx(ctx)r.Response.Writef("Hello %s! Your Age is %d",req.Name,req.Age,)return
}func main() {s := g.Server()s.Group("/", func(group *ghttp.RouterGroup) {group.Bind(new(Hello),)})s.SetPort(8000)s.Run()
}
在本示例中:
通过s.Group的分组路由方式定义一组路由注册,在其回调方法中注册的所有路由,都会带有其定义的分组路由前缀/。
通过group.Bind方法注册路由对象,该方法将会遍历路由对象的所有公开方法,读取方法的输入输出结构体定义,并对其执行路由注册。
运行后,我们访问 http://127.0.0.1:8000/?name=john&age=18 可以看到,页面输出结果符合预期。
中间件试用
中间件的类型分为两种:前置中间件和后置中间件。前置即在路由服务函数调用之前调用,后置即在其后调用。
func Middleware(r *ghttp.Request) {// 前置中间件处理逻辑 r.Middleware.Next()// 后置中间件处理逻辑
}
在中间件中执行完成处理逻辑后,使用 r.Middleware.Next() 方法进一步执行下一个流程; 如果这个时候直接退出不调用 r.Middleware.Next() 方法的话,将会退出后续的执行流程(例如可以用于请求的鉴权处理)。
优雅返回json
package mainimport ("context""fmt""github.com/gogf/gf/v2/frame/g""github.com/gogf/gf/v2/net/ghttp"
)type Response struct {Message string `json:"message" dc:"消息提示"`Data interface{} `json:"data" dc:"执行结果"`
}type HelloReq struct {g.Meta `path:"/" method:"get"`Name string `v:"required" json:"name" dc:"姓名"`Age int `v:"required" json:"age" dc:"年龄"`
}
type HelloRes struct {Content string `json:"content" dc:"返回结果"`
}type Hello struct{}func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {res = &HelloRes{Content: fmt.Sprintf("Hello %s! Your Age is %d",req.Name,req.Age,),}return
}func ResponseMiddleware(r *ghttp.Request) {r.Middleware.Next()var (msg stringres = r.GetHandlerResponse()err = r.GetError())if err != nil {msg = err.Error()} else {msg = "OK"}r.Response.WriteJson(Response{Message: msg,Data: res,})
}func main() {s := g.Server()s.Group("/", func(group *ghttp.RouterGroup) {group.Middleware(ResponseMiddleware)group.Bind(new(Hello),)})s.SetPort(8000)s.Run()
}
在中间件中:
通过r.GetHandlerResponse()
方法获取路由函数的执行结果,即路由函数返回的第一个结果参数*HelloRes
。
通过r.GetError()
获取路由函数的执行状态,即路由函数返回的第二个结果参数error,如果该结果不为nil,表示该路由函数执行产生了错误。
通过r.Response.WriteJson
将结果整合到统一的返回数据结构Response,并编码为json格式返回给调用端。
生成接口文档
type HelloReq struct {g.Meta `path:"/" method:"get" tags:"Test" summary:"Hello world test case"`Name string `v:"required" json:"name" dc:"姓名"`Age int `v:"required" json:"age" dc:"年龄"`
}
type HelloRes struct {Content string `json:"content" dc:"返回结果"`
}
可以看到,基于前面章节的接口数据结构定义,我们在其g.Meta中增加了两个标签:
tags: 该接口属于哪个分类,或者接口模块。
summary: 接口描述。
项目脚手架
框架提供的开发工具为开发者提供了便捷的开发指令简化开发工作,提供了例如工程脚手架、代码自动生成、工具及框架更新等实用命令。工具下载地址: https://github.com/gogf/gf/releases
具体教程 看https://goframe.org/quick/scaffold-index
创建项目模板
gf init demo -u
该命令创建一个工程脚手架目录,项目名称是 demo,其中的 -u 参数用户指定是否更新项目中使用的 goframe 框架为最新版本。 GoFrame框架有独特的项目工程结构
此外还可以 根据数据库表 自动生成entity、do、dao层 以及接口文档等
项目结构、流转流程、规范(如dao和service层如何写)
https://goframe.org/docs/design/project-structure
这个写得很好 建议反复阅读
我自己写的内容
快速开始
首先生成dao、do、entity
使用cli 默认生成
生成控制器目录文件
如果你想RESTFUL方式通过下面的路径调用接口
GET 'http://127.0.0.1:8000/user/1'
那么首先,在api/接口名 下面新建文件
api/user/v1/user.gotype 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"`
}
其中,v1代表版本1 方便日后维护 修改版本,但是v1不体现在客户端的访问链接中
然后使用cli工具生成controller代码
会自动生成下面代码
/Users/john/Temp/demo/
为项目根目录
$ make ctrl
generated: /Users/john/Temp/demo/api/user/user.go
generated: /Users/john/Temp/demo/internal/controller/user/user.go
generated: /Users/john/Temp/demo/internal/controller/user/user_new.go
generated: /Users/john/Temp/demo/internal/controller/user/user_v1_get_one.go
接口抽象文件/api/user/user.go——一般无需关注
其中 /api/user/user.go
定义了api interface,用于保证控制器实现的接口完整性,避免controller缺失部分接口实现的问题。由于GoFrame是一款严谨的开发框架,这种细节控制得比较好,至于该特性开发者使用与否,可以根据自身场景和需要来选择。
api/user/user.go
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================package userimport ("context""demo/api/user/v1"
)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)
}
路由对象管理user.go、user_new.go——一般无需关注
用于管理控制器的初始化,以及一些控制内部使用的数据结构、常量定义。
generated: /项目根目录/internal/controller/user/user.go
generated: /项目根目录/internal/controller/user/user_new.go
其中internal/controller/user/user.go
是一个空的源码文件,可用于定义一些控制器内部使用的数据结构、常量等内容。
internal/controller/user/user.go
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================package user
另一个internal/controller/user/user_new.go
文件是自动生成的路由对象创建文件。
internal/controller/user/user_new.go
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================package userimport ("demo/api/user"
)type ControllerV1 struct{}func NewV1() user.IUserV1 {return &ControllerV1{}
}
这两个文件都只会生成一次,随后开发者可以随意修改、扩展。
如果后续我们需要定义v2接口,make ctrl命令会类似生成type ControllerV2 struct{}结构体定义,以及func NewV2() user.IUserV2初始化方法。
controller路由实际实现代码——需要重点关注
用于具体的api接口实现的代码文件。默认情况下,会按照一个api接口一个源码文件的形式生成代码。当然,也可以控制按照api文件定义的接口聚合生成到对应的一个源码文件中。具体的命令介绍及配置请参考章节 接口规范-gen ctrl——https://goframe.org/docs/cli/gen-ctrl
generated: /项目根目录/internal/controller/user/user_v1_get_one.go
自动为我们生成的代码可能如下 :
package userimport ("context""demo/api/user/v1""demo/internal/dao"
)func (c *ControllerV1) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) {return nil, gerror.NewCode(gcode.CodeNotImplemented)
}
然后在此基础上修改即可,比如:
package userimport ("context""demo/api/user/v1""demo/internal/dao"
)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
}
数据查询接口中,我们使用了Scan方法,该方法可以将查询到的单条数据表记录智能地映射到结构体对象上。大家需要注意这里的&res.User中的User属性对象其实是没有初始化的,其值为nil。如果查询到了数据,Scan方法会对其做初始化并赋值,如果查询不到数据,那么Scan方法什么都不会做,其值还是nil
配置文件
工具配置 hack/config.yaml
在前面的章节我们已经有过介绍。这个配置文件主要是本地开发时候使用,当cli脚手架工具执行时会自动读取其中的配置内容。
示例源码:https://github.com/gogf/quick-demo/blob/main/hack/config.yaml
业务配置 manifest/config/config.yaml
主要维护业务项目的组件配置信息、业务模块配置,完全由开发者自行维护。在程序启动时会读取该配置文件。该业务 默认的脚手架项目模板提供的业务配置如下:
manifest/config/config.yaml
# 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"
默认提供了3项组件的配置,分别为:
server:Web Server的配置。这里默认配置的监听地址为:8000,并启用了接口文档特性。
logger:默认日志组件的配置。这里的日志级别是所有日志都会打印,并且都会输出到标准输出。
database:数据库组件的配置。这只是一个模板,需要我们根据实际情况修改链接地址。
每一项组件配置的注释上提供了官网文档的配置参考链接。我们这里需要修改数据库配置中的链接信息,为我们真实可用的链接信息。关于数据库的配置详细介绍,感兴趣请参考章节:ORM使用配置-配置文件
示例源码:https://github.com/gogf/quick-demo/blob/main/manifest/config/config.yaml
添加路由注册
添加我们新填写的api到路由非常简单,如下:,在分组路由的group.Bind方法中,通过user.NewV1()添加我们的路由对象即可。
其他
在脚手架项目模板main.go的import中有一段_ “demo/internal/packed”,表示GoFrame框架的资源管理,这是一个高级特性。该特性可以将任何资源打包进二进制文件,这样我们在发布的时候,仅需要发布一个二进制文件即可。我们这里没有用到该特性,因此大家了解即可,感兴趣可以后续查阅开发手册相关章节。