go的各个开发框架比较多,个人最喜欢的是gin和gofrane,转载自https://gitee.com/unlimited13/code bilibili有教学视频
视频教学
文章目录
准备工作前置条件安装框架工具项目初始化项目启动框架设计项目目录结构
路由路由注册函数注册对象注册分组注册
规范路由方法签名示例
获取请求输入普通请求输入query参数获取表单参数获取(POST参数获取)动态路由参数获取所有请求参数获取
Api请求输入自定义请求方式1. **查询参数 (Query Parameters)**2. **请求体 (Body)**3. **表单参数 (Form Parameters)**4. **RESTful 方式**总结
响应输出文本数据返回JSON数据返回模板内容返回API数据返回
数据库数据库准备数据库配置驱动添加与导入数据库基本操作查询数据One/All/Count/Value/Array/FieldsMax/Min/Sum/AvgWhere/Where*/WhereOr/WhereOr*Group/Order/Order*Scan查询结果为空判断分页
多表联查插入数据Insert/Replace/SaveInsertAndGetIdgdb.Raw
更新数据UpdateIncrement/Decrement
删除数据
时间维护与软删除事务处理原生SQL的使用DAO自动生成与使用字段过滤关联查询
模板引擎简单使用示例模板配置静态资源条件判断循环
上传与下载文件上传文件下载上传限制
数据校验Cookie/Sessiongolang-jwt中间件组件数据结构时间随机数
接口文档构建打包微服务部分
<br
准备工作
前置条件
已安装Go语言开发环境,已配置好GOROOT、GOPATH环境变量
熟悉Go语言基本语法与使用
GoFrame文档:https://goframe.org/
学习过程以官方文档为主,本文内容均摘自官方文档,
本阶段只介绍Web开发部分,微服务部分以后有机会新开
安装框架工具
https://github.com/gogf/gf/releases
下载对应的包安装。推荐安装到GOROOT的bin目录中
用以下命令查看是否安装成功
gf -v
1
项目初始化
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn
# 如果已经设置过可以不要上面这两行
gf init gf_demo -u # 如果之前已经创建过项目,并且不需要创建最新版本则省略-u
12345
常用代理地址:
https://goproxy.cn
https://goproxy.io
https://mirrors.aliyun.com/goproxy/
项目启动
进入项目中main.go文件所在的目录运行如下命令
gf run main.go
1
启动成功后,在浏览器中输入http://127.0.0.1:8000/hello
查看结果
框架设计
关于框架设计的内容,有点过于抽象,内容也是偏理论的,初学就来纠结这部分基本上也难以理解,所以这部分的其他内容可以放到以后再来研究。不过也需要了解一点基础知识,比如MVC
与3-Tier Architecture
,这部分内容详见文档代码分层设计,不需要完全理解,知道个大概也就可以了。
项目目录结构
/ ├── api 请求接口输入/输出数据结构定义 ├── hack 项目开发工具、脚本 ├── internal 业务逻辑存放目录,核心代码 │ ├── cmd 入口指令与其他命令工具目录 │ ├── consts 常量定义目录 │ ├── controller 控制器目录,接收/解析用户请求 │ ├── dao 数据访问对象目录,用于和底层数据库交互 │ ├── logic 核心业务逻辑代码目录 │ ├── model 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 │ | ├── do 数据操作中业务模型与实例模型转换,由工具维护,不能手动修改 │ │ └── entity 数据模型是模型与数据集合的一对一关系,由工具维护,不用手动修改。 │ └── service 业务接口定义层。具体的接口实现在logic中进行注入。 ├── manifest 包含程序编译、部署、运行、配置的文件 ├── resource 静态资源文件 ├── utility ├── go.mod └── main.go 程序入口文件
有关项目目录更多详细介绍以及请求分层流转见文档工程目录设计
路由
路由注册
函数注册
相关方法:
func (s *Server) BindHandler(pattern string, handler interface{})
1
其中handler
的定义方式有如下两种:
func(request *ghttp.Request)
func(ctx context.Context, BizRequest)(BizResponse, error)
12
匿名函数与普通函数注册
internal/cmd/cmd.go
package cmd
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
)
func handler(req *ghttp.Request) {
req.Response.Writeln("<h1>Hello World From handler</h1>")
}
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.BindHandler("/hello", func(req *ghttp.Request) {
req.Response.Writeln("<h1>Hello World!</h1>")
})
// 或者使用提前定义好的函数来进行注册
s.BindHandler("/world", handler)
s.Run()
return nil
},
}
)
1234567891011121314151617181920212223242526272829303132333435
注册成功后在浏览器输入http://127.0.0.1:8000/hello或者http://127.0.0.1:8000/world即可访问对应的路由
指定HTTP请求方法
上述方法注册路由默认支持所有HTTP请求方法,如果需要指定请求方法,可用以下写法:
// 该路由只支持GET请求
s.BindHandler("GET:/hello", func(req *ghttp.Request) {
req.Response.Writeln("<h1>Hello World! GET</h1>")
})
// 该路由只支持POST请求
s.BindHandler("POST:/hello", func(req *ghttp.Request) {
req.Response.Writeln("<h1>Hello World! POST</h1>")
})
12345678
对于同一路由可以定义不同的请求方法实现不同功能。
几个最常用HTTP方法
方法 描述 GET 用于获取数据,不会修改服务端资源数据 POST 将资源数据提交到服务端,常用于在服务端创建新数据 PUT 将资源数据提交到服务端,常用于修改已存在的资源数据 DELETE 用于删除服务端资源数据
对象方法注册
还可以用对象当中的方法来注册路由。
选定义一个名为user
控制器
internal/controller/user/user.go
package user
import "github.com/gogf/gf/v2/net/ghttp"
type Controller struct{}
func New() *Controller {
return &Controller{}
}
func (c *Controller) AddUser(r *ghttp.Request) {
r.Response.Writeln("添加用户")
}
func (c *Controller) UpdateUser(r *ghttp.Request) {
r.Response.Writeln("更新用户")
}
func (c *Controller) DeleteUser(r *ghttp.Request) {
r.Response.Writeln("删除用户")
}
func (c *Controller) ListUser(r *ghttp.Request) {
r.Response.Writeln("用户列表")
}
func (c *Controller) GetUser(r *ghttp.Request) {
r.Response.Writeln("查询一个用户")
}
func (c *Controller) Post(r *ghttp.Request) {
r.Response.Writeln("添加用户")
}
func (c *Controller) Put(r *ghttp.Request) {
r.Response.Writeln("更新用户")
}
func (c *Controller) Delete(r *ghttp.Request) {
r.Response.Writeln("删除用户")
}
func (c *Controller) Get(r *ghttp.Request) {
r.Response.Writeln("查询一个用户")
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445
internal/cmd/cmd.go
package cmd
import (
"context"
// 引入控制器user包
"gf_demo/internal/controller/user"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcmd"
)
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()
// 定义对象
usercontroller := user.New()
// 将对象方法绑定到路由
s.BindHandler("/adduser", usercontroller.AddUser)
s.Run()
return nil
},
}
)
123456789101112131415161718192021222324252627282930
对象注册
对象里的方法可以批量注册
相关方法
func (s *Server) BindObject(pattern string, object interface{}, method ...string)
func (s *Server) BindObjectMethod(pattern string, object interface{}, method string)
func (s *Server) BindObjectRest(pattern string, object interface{})
12345
绑定全部公共方法
internal/cmd/cmd.go
package cmd
import (
"context"
"starting/internal/controller/user"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcmd"
)
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()
usercontroller := user.New()
// 绑定user控制器中所有公共方法
s.BindObject("/user", usercontroller)
s.Run()
return nil
},
}
)
123456789101112131415161718192021222324252627
绑定指定方法
usercontroller := user.New()
// 绑定user控制器中多个方法
s.BindObject("/user", usercontroller, "AddUser,UpdateUser")
// 绑定单个方法
s.BindObjectMethod("/deluser", usercontroller, "DeleteUser")
12345
以RESTFul方绑定对象方法
usercontroller := user.New()
s.BindObjectRest("/user", usercontroller)
12
分组注册
可以为不同路由设置一个相同的前缀,即分组路由,分组路由有以下两种写法
s := g.Server()
usercontroller := user.New()
s.Group("/user", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
usercontroller, // 绑定到控制器对象
)
// 可以用GET POST PUT等定义路由
group.GET("/get", func(r *ghttp.Request) {
r.Response.Writeln("/user/get")
})
})
s.Run()
12345678910111213141516
s := g.Server()
usercontroller := user.New()
group := s.Group("/user")
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
usercontroller, // 绑定到控制器对象
)
// 可以用GET POST PUT等定义路由
group.GET("/get", func(r *ghttp.Request) {
r.Response.Writeln("/user/get")
})
s.Run()
12345678910111213
s.BindObjectMethod("/deleteDevice", dcontroller, "DeleteDevice")
// 绑定restful风格的路由
/**个悲剧名字
GET /device1 对应 getDevice() 方法
POST /device1 对应 createDevice() 方法
PUT /device1 对应 updateDevice() 方法
DELETE /device1 对应 deleteDevice() 方法
*/
//s.BindObjectRest("/device1", dcontroller)
//分组路由
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(hello.NewV1(),)
})
s.Group("/device", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
//绑定resitful风格的路由 但是方法很多时候都是all 还是建议手动定义请求方式
//group.Bind(
// dcontroller, // 绑定到控制器对象
//)
// 手动定义可以用GET POST PUT等定义路由
group.GET("/list", dcontroller.ListDevices)
})
1234567891011121314151617181920212223
规范路由
GoFrame中提供了规范化的路由注册方式,注册方法如下
func Handler(ctx context.Context, req *Request) (res *Response, err error)
1
其中Request
与Response
为自定义的结构体。
方法签名示例
方法签名需要符合 GoFrame 的要求,可以选择以下两种之一:
func(context.Context, *ReqStruct) (*ResStruct, error)
func(*ghttp.Request)
一般不适用规范路由的化就是第二种
通过如下方式指定请求方法与路径
type HelloReq struct {
g.Meta `path:"/hello" method:"get"`
}
123
定义请求体和响应体
// 请求结构体
type DevicesReq struct {
g.Meta `path:"/device/list" method:"get" summary:"获取设备列表"`
Page int `v:"required|min:1" json:"page"` // 页码参数,最小值为1
Limit int `v:"required|min:1|max:100" json:"limit"` // 每页的记录数,范围1到100
}
// 响应结构体
type DeviceRes struct {
g.Meta `mime:"application/json" example:"{}"`
Message string `json:"message"`
Result api.Result
}
//包裹一个基础的响应体
type Result struct {
Status int `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
}
1234567891011121314151617181920
然后controller 路由层进行接收 (这里是吧路由层和逻辑层做同一处理了 正常来说路由只负责在接收前端的传递和后端的返回)
func (c *DeviceController) ListDevices(ctx context.Context, req *device.DevicesReq) (*device.DeviceRes, error) {
// 实际业务逻辑
fmt.Printf("接收到的请求参数分页参数:%d
,页码大小为:%d
", req.Page, req.Limit)
data := api.Result{
Code: 0,
Message: "success",
Data: []string{"Device1", "Device2"}, // 示例数据
}
g.RequestFromCtx(ctx).Response.WriteJson(&device.DeviceRes{
Message: "获取设备列表成功",
Result: data,
})
return nil, nil
}
123456789101112131415
然后再cmd文件 注册时候 即可直接绑定controller 即可无需再进行额外绑定 因为该conller的请求中有这个元数据标识
并且根据官方的初始化demo 也是有点像java
快速开始 – GoFrame (ZH)-Latest – GoFrame官网 – 类似PHP-Laravel, Java-SpringBoot的Go企业级开发框架
/*
定义处理逻辑接口
*/
type IHelloV1 interface {
Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error)
}
123456
获取请求输入
普通请求输入
基础代码如下:路由用group绑定到控制器后,在控制器中写如下方法,以下代码均在此修改:
func (c *Controller) Params(request *ghttp.Request) {
m := request.GetQueryMap()
request.Response.WriteJson(m)
}
1234
query参数获取
query参数是指以?a=1&b=2
的形式写在url中的参数,通常由GET方法传递。
单个参数值
m := request.GetQuery("name")
1
GetQuery可以指定参数名称,获取对应的参数值,如果值不存在,则返回nil
还可以指定默认值,当对应参数值不存在时,返回指定的默认值
m := request.GetQuery("name", "孙行者")
1
返回的是一个gvar.Var
类型,可以根据需要进行类型转换,常用类型转换方法如下
func (v *Var) Bytes() []byte
func (v *Var) String() string
func (v *Var) Bool() bool
func (v *Var) Int() int
func (v *Var) Int8() int8
func (v *Var) Int16() int16
func (v *Var) Int32() int32
func (v *Var) Int64() int64
func (v *Var) Uint() uint
func (v *Var) Uint8() uint8
func (v *Var) Uint16() uint16
func (v *Var) Uint32() uint32
func (v *Var) Uint64() uint64
func (v *Var) Float32() float32
func (v *Var) Float64() float64
func (v *Var) Time(format ...string) time.Time
func (v *Var) Duration() time.Duration
func (v *Var) GTime(format ...string)
123456789101112131415161718
批量获取Query参数
GoFrame中提供了GetQueryMap
,GetQueryMapStrStr
,GetQueryMapStrVar
三个方法用于批量获取Query参数,三个方法使用方式一致,只是返回类型不同。
获取全部Query参数
m := request.GetQueryMap()
1
指定需要获取的参数名称与默认值
m := request.GetQueryMap(map[string]interface{}{"name": "者行孙", "age": 600})
1
将Query参数转化为自定义结构体
可以自定义结构体,将请求参数直接转化为对应的结构体:
type user struct {
Name string
Age int
}
var u *user
err := request.ParseQuery(&u)
if err != nil {
request.Response.WritelnExit("转换出借")
}
123456789
如上,结构体中成员为Name
、Age
,参数为name
和age
则参成功转换,如果结构体成员变量名与参数名不一致则无法转换,此时需要为成员变量指定其对应的参数,可以用json:
/param:
/p:
这些方式来指定。如下
type user struct {
UserName string `json:"name"`
UserAge int `p:"age"`
}
1234
表单参数获取(POST参数获取)
表单参数获取是指获取application/x-www-form-urlencoded
、application/form-data
、multipart/form-data
等数据,也可以用来获取以json格式提交的数据,简单理解即为可以获取POST方法提交的数据。
单个参数
m := request.GetForm("name")
1
GetForm
用于指定参数名称,获取对应参数值,如果对应参数不存在,返回nil
也可以指定默认值,当指定参数不存在时,返回默认值
m := request.GetForm("name", "烧包谷")
1
返回的是一个gvar.Var
类型,可以根据需要进行类型转换
批量获取请求数据
可以用GetFormMap
、GetFormMapStrStr
、GetFormMapStrVar
批量获取请求数据,三个方法使用方式一样,只是返回的Map类型不同。
m := request.GetFormMap()
1
可以指定需要获取的参数以及默认值
m := request.GetFormMap(map[string]interface{}{"name": "大洋芋"})
1
将请求数据转化为自定义结构体
和Query参数一样,也可以将请求参数直接转为自定义结构体。如果结构体成员名称与参数名称不一致,也可以用json:
、param:
、p:
这些tag来指定对应的参数名称
type user struct {
UserName string `json:"name"`
UserAge int `p:"age"`
}
var u *user
err := request.ParseForm(&u)
if err != nil {
request.Response.WritelnExit("转换出借")
}
123456789
动态路由参数获取
动态路由需要对现有代码进行一点改动,需要先在api
包中定义请求与返回数据格式,对指定的路由进行动态注册:
api
package api
import (
"github.com/gogf/gf/v2/frame/g"
)
type Res struct {
g.Meta `mime:"text/html"`
}
type ParamReq struct {
g.Meta `path:"/params/:name" method:"all"`
}
12345678910111213
再将控制器的的方法利用api
数据结构进行修改:
Controller
func (c *Controller) Params(ctx context.Context, req *api.ParamReq) (res *api.Res, err error) {
request := g.RequestFromCtx(ctx)
u := request.GetRouter("name")
request.Response.WriteJson(g.Map{"data": u})
return
}
123456
获取单个参数
u := request.GetRouter("name")
1
返回gvar.Var
类型,可以按需要进行类型转换。也可以指定默认值。
批量获取参数
u := request.GetRouterMap()
1
返回值为map[string]string
。如果没有设置动态路由,则返回nil
所有请求参数获取
GoFrame中还提供了一些方法获取所有请求参数,用法与上面两种类似,只是不区分请求方法。如果GET和POST提供的参数名称相同,则POST参数优先。
获取单个参数
data := request.GetRequest("name")
// 简写
data := request.Get("name")
123
返回gvar.Var
类型,可以提供默认值。
批量获取请求参数
data := request.GetRequestMap()
// 可以指定需要获取的参数名及默认值
data := request.GetRequestMap(g.Map{"name": ""})
// 还有以下几种
data := request.GetRequestMapStrStr()
data := request.GetRequestMapStrVar()
123456
将请求参数转为自定义结构体
request.Parse(&u) // u为自定义结构体指针
1
Api请求输入
在api
中定义请求与响应数据结构,可以直接将需要接收的参数定义为请求结构体的成员,请求时会自动转为对应结构体。
例如,将前面的api
请求部分改为
type ParamReq struct {
g.Meta `path:"/params" method:"post"`
UserName string `p:"name" d:"林冲"`
UserAge int `p:"age" d:"110"`
}
12345
其中p:
或param:
用于指定该成员对应的请求参数名,d:
或default:
用于指定默认值。如果Query与Body中有相同名称的参数,则以Body中的参数优先。
自定义请求方式
在 GoFrame 中,你可以通过不同的方式接收和处理请求参数,例如通过查询参数(Query)、请求体(Body)、表单(Form)等。下面是如何在 GoFrame 中处理这些不同类型的请求参数的示例:
1. 查询参数 (Query Parameters)
查询参数通常是 URL 中的 ?key=value
部分。例如,/device/list?page=1&limit=10
。
type DevicesReq struct {
g.Meta `path:"/device/list" method:"get" summary:"获取设备列表"`
Page int `json:"page"` // 页码参数
Limit int `json:"limit"` // 每页的记录数
}
func (c *DeviceController) ListDevices(ctx context.Context, req *DevicesReq) (*DeviceRes, error) {
// 处理查询参数
fmt.Printf("Page: %d, Limit: %d
", req.Page, req.Limit)
// 返回响应
return &DeviceRes{Message: "获取设备列表成功"}, nil
}
123456789101112
2. 请求体 (Body)
对于 POST 或 PUT 请求,通常使用请求体传递参数。可以使用结构体绑定请求体数据。请确保使用合适的 method
标记(例如 method:"post"
或 method:"put"
)。
go复制代码type DevicesReq struct {
g.Meta `path:"/device/create" method:"post" summary:"创建设备"`
Name string `json:"name" v:"required"` // 设备名称
Type string `json:"type" v:"required"` // 设备类型
}
func (c *DeviceController) CreateDevice(ctx context.Context, req *DevicesReq) (*DeviceRes, error) {
// 处理请求体数据
fmt.Printf("Name: %s, Type: %s
", req.Name, req.Type)
// 返回响应
return &DeviceRes{Message: "设备创建成功"}, nil
}
123456789101112
3. 表单参数 (Form Parameters)
表单参数通常来自 HTML 表单提交。GoFrame 支持表单参数的解析。使用 form
标签标记字段。
type DevicesReq struct {
g.Meta `path:"/device/update" method:"post" summary:"更新设备"`
ID int `json:"id" form:"id" v:"required"` // 设备ID
Name string `json:"name" form:"name"` // 设备名称
}
func (c *DeviceController) UpdateDevice(ctx context.Context, req *DevicesReq) (*DeviceRes, error) {
// 处理表单参数
fmt.Printf("ID: %d, Name: %s
", req.ID, req.Name)
// 返回响应
return &DeviceRes{Message: "设备更新成功"}, nil
}
123456789101112
4. RESTful 方式
RESTful 风格的请求通常包含路径参数。例如 /device/{id}
。可以通过 path
标记定义路径参数。
// 请求结构体
type DevicesReq struct {
g.Meta `path:"/device/list/{id}" method:"get" summary:"获取设备列表"`
Page int `v:"required|min:1" d:"1" json:"page"` // 页码参数,最小值为1
Limit int `v:"required|min:1|max:100" d:"10" json:"limit"` // 每页的记录数,范围1到100
Id int `d:"10001" json:"id"`
}
func (c *DeviceController) GetDeviceDetail(ctx context.Context, req *DeviceDetailReq) (*DeviceRes, error) {
// 处理路径参数
fmt.Printf("ID: %d
", req.ID)
// 返回响应
return &DeviceRes{Message: "获取设备详情成功"}, nil
}
123456789101112131415
总结
查询参数: 适用于 GET
请求,通过 URL 传递。可以直接映射到结构体字段。请求体: 适用于 POST
, PUT
, PATCH
请求,传递 JSON 或其他格式的数据。使用结构体接收。表单参数: 适用于 POST
请求,表单提交的数据。使用 form
标签标记字段。RESTful 路径参数: 通过路径参数传递,适用于获取特定资源或执行特定操作。使用 path
标签标记字段。
响应输出
在控制器中新建如下方法,用来测试响应输出。以下所有代码均在此处修改。
func (c *Controller) Resp(req *ghttp.Request) {
// 以下代码在此写
}
123
文本数据返回
GoFrame中通过以下方法返回文本数据到客户端:
// Write 将指定的内容写入响应体。此方法支持多个参数,并按提供的顺序将它们写入响应。
// 用法示例: r.Write(content1, content2, ...)
func (r *Response) Write(content ...interface{})
// WriteExit 将指定的内容写入响应体,并随后终止请求处理。此方法在写入响应后停止进一步处理。
// 用法示例: r.WriteExit(content1, content2, ...)
func (r *Response) WriteExit(content ...interface{})
// Writef 使用指定的格式字符串格式化内容,并将其写入响应体。此方法类似于 fmt.Printf,支持格式化字符串。
// 用法示例: r.Writef("Hello %s!", "world")
func (r *Response) Writef(format string, params ...interface{})
// WritefExit 使用指定的格式字符串格式化内容,将其写入响应体,并随后终止请求处理。此方法在格式化并写入响应后停止进一步处理。
// 用法示例: r.WritefExit("Hello %s!", "world")
func (r *Response) WritefExit(format string, params ...interface{})
// Writeln 将指定的内容写入响应体,并在内容末尾添加换行符。此方法适用于需要换行的内容。
// 用法示例: r.Writeln("Hello", "world")
func (r *Response) Writeln(content ...interface{})
// WritelnExit 将指定的内容写入响应体,并在内容末尾添加换行符,然后终止请求处理。此方法在写入内容并添加换行符后停止进一步处理。
// 用法示例: r.WritelnExit("Hello", "world")
func (r *Response) WritelnExit(content ...interface{})
// Writefln 使用指定的格式字符串格式化内容,将其写入响应体,并在内容末尾添加换行符。此方法类似于 Writef,但在内容末尾添加了换行符。
// 用法示例: r.Writefln("Hello %s!", "world")
func (r *Response) Writefln(format string, params ...interface{})
// WriteflnExit 使用指定的格式字符串格式化内容,将其写入响应体,并在内容末尾添加换行符,然后终止请求处理。此方法在格式化内容并添加换行符后停止进一步处理。
// 用法示例: r.WriteflnExit("Hello %s!", "world")
func (r *Response) WriteflnExit(format string, params ...interface{})
12345678910111213141516171819202122232425262728293031323334353637383940
以上方法中,带有Exit
的表示执行完响应之后就退出本次请求,不再执行后面的内容。带有ln
的表示会在响应内容的末尾追加换行符。
以上方法用于向客户端响应文本内容。内容格式为text/html
或text/plain
,参数可以是任意数据类型,非字符串类型通常会将内容进行json转为字符串后返回到客户端。
如果提供参数为文本,可以是普通文本也可以是HTML文本。
响应简单文本
req.Response.Write("锦瑟无端五十弦")
1
响应简单HTML
req.Response.Write("<h1>春蚕到死丝方尽</h1>")
1
响应复杂HTML
html := `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Clock</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap');
*
{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body
{
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #acbaca;
}
.clock
{
position: relative;
width: 300px;
height: 300px;
background: #c9d5e0;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50px;
box-shadow: 30px 30px 30px -10px rgba(0,0,0,0.15),
inset 15px 15px 10px rgba(255,255,255,0.75),
-15px -15px 35px rgba(255,255,255,0.55),
inset -1px -1px 10px rgba(0,0,0,0.2);
}
.clock::before
{
content: '';
position: absolute;
width: 4px;
height: 4px;
background: #e91e63;
border-radius: 50%;
z-index: 1000;
box-shadow: 0 0 0 1px #e91e63,
0 0 0 3px #fff,
0 0 5px 5px rgba(0,0,0,0.15);
}
.clock .numbers
{
position: absolute;
inset: 35px;
background: #152b4a;
border-radius: 50%;
box-shadow: 5px 5px 15px #152b4a66,
inset 5px 5px 5px rgba(255,255,255,0.55),
-6px -6px 10px rgba(255,255,255,1);
}
.clock .numbers span
{
position: absolute;
inset: 5px;
text-align: center;
color: #fff;
font-size: 1.25em;
transform: rotate(calc(90deg * var(--i)));
}
.clock .numbers span b
{
font-weight: 600;
display: inline-block;
transform: rotate(calc(-90deg * var(--i)));
}
.clock .numbers::before
{
content: '';
position: absolute;
inset: 35px;
background: linear-gradient(#2196f3,#e91e63);
border-radius: 50%;
animation: animate 2s linear infinite;
}
@keyframes animate
{
0%
{
transform: rotate(360deg);
}
100%
{
transform: rotate(0deg);
}
}
.clock .numbers::after
{
content: '';
position: absolute;
inset: 38px;
background: #152b4a;
border-radius: 50%;
}
.clock .numbers .circle
{
position: absolute;
inset: 0;
border-radius: 50%;
display: flex;
justify-content: center;
z-index: 10;
}
.clock .numbers .circle i
{
position: absolute;
width: 3px;
height: 50%;
background: #fff;
transform-origin: bottom;
}
.clock .numbers .circle#hr i
{
transform: scaleY(0.3);
width: 4px;
}
.clock .numbers .circle#mn i
{
transform: scaleY(0.45);
}
.clock .numbers .circle#sc i
{
width: 2px;
transform: scaleY(0.55);
background: #e91e63;
box-shadow: 0 30px 0 #e91e63;
}
</style>
</head>
<body>
<div class="clock">
<div class="numbers">
<span style="--i:0;"><b>12</b></span>
<span style="--i:1;"><b>3</b></span>
<span style="--i:2;"><b>6</b></span>
<span style="--i:3;"><b>9</b></span>
<div class="circle"><i></i></div>
<div class="circle"><i></i></div>
<div class="circle"><i></i></div>
</div>
</div>
<script>
let hr = document.querySelector('#hr');
let mn = document.querySelector('#mn');
let sc = document.querySelector('#sc');
setInterval(()=>{
let day = new Date();
let hh = day.getHours() * 30;
let mm = day.getMinutes() * 6;
let ss = day.getSeconds() * 6;
hr.style.transform = 'rotateZ(' + hh+(mm/12) + 'deg)';
mn.style.transform = 'rotateZ(' + mm + 'deg)';
sc.style.transform = 'rotateZ(' + ss + 'deg)';
})
</script>
</body>
</html>
`
req.Response.Write(html)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
格式化数据填充
html := `
<div>姓名:%s</div>
<div>年龄:%d</div>
`
req.Response.Writef(html, "林黛玉", 16)
12345
JSON数据返回
GoFrame中可以通过以下方法返回JSON数据:
func (r *Response) WriteJson(content interface{})
func (r *Response) WriteJsonExit(content interface{})
12
通过以上方法,会直接将参数内容进行JSON转换之后返回到客户端,并且将响应头中Content-Type
设置为 application/json
。
在路由规范哪里提到的 ,如果是复合要求的自定义请求体和响应体 无需在把json 写回流中
func (c *TemplateController) Tpl(ctx context.Context, r *template.TemReq) (res *template.TemRes, err error) {
//rWriteTplDefault() // 解析并返回默认模板文件内容
// 解析并返回模板字符串
//g.RequestFromCtx(ctx).Response.WriteTplContent("<h1>你好, {{.name}} 欢迎学习{{.lang}}</h1>", g.Map{"name": "王道长", "lang": "GoFrame"})
response := &template.TemRes{
Message: "操作成功",
Code: 200,
Data: g.Map{"name": "王道长", "lang": "GoFrame"},
}
return response, nil
}
123456789101112
当然如果要写响应相关逻辑的话
func (c *TemplateController) Tpl(ctx context.Context, r *template.TemReq) (*template.TemRes, error) {
response := &template.TemRes{
Message: "操作成功",
Code: 200,
Data: g.Map{"name": "王道长", "lang": "GoFrame"},
}
response1 := &template.TemRes{
Message: "操失败",
Code: 200,
Data: g.Map{"name": "JACK", "lang": "GoFrame"},
}
// 业务逻辑处理,例如错误检查
if r == nil {
return nil, errors.New("request cannot be nil")
}
// 获取ghttp.Request对象
request := ghttp.RequestFromCtx(ctx)
// 设置响应为JSON格式,错误处理
request.Response.WriteJson(response1)
// 此处返回response对象和nil错误,表示处理成功
return response, nil
}
123456789101112131415161718192021222324252627
返回的是request.Response.WriteJson(response1)的内容
模板内容返回
前面可以用writef
将数据格式化到HTML内容当中,但这样的做法对于数据以及HTML文件较多的情况太过于麻烦,因此Web框架中一般会采用模板引擎,使用模板语言来进行数据渲染,简化HTML页面与后端数据的交互。
GoFrame中用以下方法进行模板解析和返回:
func (r *Response) WriteTpl(tpl string, params ...gview.Params) error
func (r *Response) WriteTplDefault(params ...gview.Params) error
func (r *Response) WriteTplContent(content string, params ...gview.Params) error
123
其中最常用的是WriteTpl
,详细内容后面模板引擎内容里面再说,现在简单理解为该方法可以读取一个html文件,并将其返回给客户端。默认模板文件存放在resource/template
下面,因此WriteTpl
的第一个参数为对应的模板html文件相对于template
的路径
示例:
req.Response.WriteTpl("index.html")
req.Response.WriteTpl("user/index.html")
12
API数据返回
现在Web应用多是前后端分离,返回数据为JSON格式,前面所说的WriteJson
这样的方法只是单纯将提供的数据进行JSON转换后返回,在实际开发中,返回的JSON数据通常为以 下结构(具体项目会有差异,但基本都是类似结构):
{
"code":0, // 自定义编码,用来表示请求成功与失败
"msg":"请求成功", // 提示信息,如果请求出错则为错误信息
"data":{} // 请求返回数据,请求出错一般为null
}
12345
GoFrame为前后端分离的API开发提供了很好的支持,只需要借助api
模块就可以方便完成类似的返回结构,不需要自行定义。
操作步骤如下:
在api
中定义请求与响应数据结构
type ApiReq struct {
g.Meta `path:"/api" method:"all"`
}
type ApiRes struct {
UserName string `json:"name"`
UserAge int `json:"age"`
List g.Array `json:"list"`
}
123456789
在控制器中定义对应的方法
func (c *Controller) Api(ctx context.Context, req *api.ApiReq) (res *api.ApiRes, err error) {
return
}
123
实例化返回数据并返回
res = &api.ApiRes{
UserName: "张三",
UserAge: 120,
List: g.Array{1, 2, 3, 4},
}
return
123456
如果有错误,定义错误信息并直接返回
err = gerror.Newf("服务器开小差了")
return
12
用上述方法返回数据,会自动返回如下格式JSON数据
{
"code":0,
"message":"",
"data":{
"name":"张三",
"age":120,
"list":[1,2,3,4]
}
}
123456789
以上数据格式是通过中间件ghttp.MiddlewareHandlerResponse
实现的,实际应用当中可以仿照这一中间件自行定义中间件来确定需要的数据返回格式。
数据库
数据库准备
需要先安装MySQL数据库(也可以使用其他数据库,本教程以MySQL为例),安装过程如果不了解的可以在B站搜一下MySQL相关教程。
创建一个goframe
数据库,字符集为utf8
运行下列SQL,创建测试数据表
USE `goframe`;
/*Table structure for table `book` */
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` VARCHAR(50) NOT NULL COMMENT '书名',
`author` VARCHAR(30) NOT NULL COMMENT '作者',
`price` DOUBLE NOT NULL COMMENT '价格',
`publish_time` DATE COMMENT '出版时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
/*Data for the table `book` */
INSERT INTO `book`(`id`,`name`,`author`,`price`) VALUES
(1,'MySQL数据库从入门到精通','王飞飞',59.8),
(2,'设计模式','刘伟',45),
(3,'数据库原理及应用','刘亮',33),
(4,'Linux驱动开发入门与实践','郑强',69),
(5,'Linux驱动开发入门与实践','郑强',69),
(6,'Linux驱动开发入门与实践','郑强',69);
/*Table structure for table `dept` */
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`pid` INT(10) UNSIGNED DEFAULT NULL COMMENT '上级部门ID',
`name` VARCHAR(30) DEFAULT NULL COMMENT '部门名称',
`leader` VARCHAR(20) DEFAULT NULL COMMENT '部门领导',
`phone` VARCHAR(11) DEFAULT NULL COMMENT '联系电话',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=108 DEFAULT CHARSET=utf8;
/*Data for the table `dept` */
INSERT INTO `dept`(`id`,`pid`,`name`,`leader`,`phone`) VALUES
(100,0,'哪都通','赵方旭','10000000000'),
(101,100,'华北大区','徐四','10000000001'),
(102,100,'东北大区','高廉','10000000002'),
(103,100,'华东大区','窦乐','10000000003'),
(104,100,'华中大区','任菲','10000000004'),
(105,100,'华南大区',NULL,NULL),
(106,100,'西北大区','华风','10000000005'),
(107,100,'西南大区','郝意','10000000006');
/*Table structure for table `emp` */
DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`dept_id` INT(10) UNSIGNED NOT NULL COMMENT '所属部门',
`name` VARCHAR(30) NOT NULL COMMENT '姓名',
`gender` TINYINT(1) DEFAULT NULL COMMENT '性别: 0=男 1=女',
`phone` VARCHAR(11) DEFAULT NULL COMMENT '联系电话',
`email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
`avatar` VARCHAR(100) DEFAULT NULL COMMENT '照片',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8;
/*Data for the table `emp` */
INSERT INTO `emp`(`id`,`dept_id`,`name`,`gender`,`phone`,`email`) VALUES
(1,100,'赵方旭',0,'10000000000','zhaofx@nadoutong.com'),
(2,100,'毕游龙',0,'10000000007','biyoulong@nadoutong.com'),
(3,100,'黄伯仁',0,'10000000008','huangboren@nadoutong.com'),
(4,101,'徐四',0,'10000000001','xusi@nadoutong.com'),
(5,101,'徐三',0,'10000000009','xusan@nadoutong.com'),
(6,101,'冯宝宝',1,'10000000010','fengbaobao@nadoutong.com'),
(7,101,'张楚岚',0,'10000000011','zhangchulan@nadoutong.com'),
(8,102,'高廉',0,'10000000002','gaolian@nadoutong.com'),
(9,102,'高二壮',1,'10000000012','gaoerzhuang@nadoutong.com'),
(10,103,'窦乐',0,'10000000003','doule@nadoutong.com'),
(11,103,'肖自在',0,'10000000013','xiaozizai@nadoutong.com'),
(12,104,'任菲',0,'10000000004','renfei@nadoutong.com'),
(13,106,'华风',0,'10000000005','huafeng@nadoutong.com'),
(14,107,'郝意',0,'10000000006','huafeng@nadoutong.com');
DROP TABLE IF EXISTS `hobby`;
CREATE TABLE `hobby` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`emp_id` INT UNSIGNED NOT NULL COMMENT 'EmpID',
`hobby` VARCHAR(50) COMMENT '爱好',
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
INSERT INTO `hobby` (`id`, `emp_id`, `hobby`) VALUES
(1, 6, '埋人'),
(2, 4, '看美女'),
(3, 7, '月下遛鸟');
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`username` VARCHAR(20) NOT NULL COMMENT '用户名',
`nickname` VARCHAR(30) COMMENT '昵称',
`password` VARCHAR(32) COMMENT '密码',
`avatar` VARCHAR(100) COMMENT '头像',
`created_at` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
INSERT INTO
`user` (`id`, `username`, `nickname`, `password`, `avatar`, `created_at`) VALUES
(1, 'libai', '李白', '123456', '', '2023-10-08 16:57:24'),
(2, 'dufu', '杜甫', '123456', '', '2023-10-08 16:57:24'),
(3, 'baijuyi', '白居易', '123456', '', '2023-10-08 16:57:24');
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
数据库配置
数据库内容准备完毕后,在配置文件中进行数据库配置,只需要添加如下的内容即可
manifest/config/config.yaml
database:
type: "mysql"
host: "127.0.0.1"
port: "3306"
user: "root"
pass: "root"
name: "goframe"
timezone: "Asia/Shanghai"
debug: true
123456789
type
:数据库类型 mysql/sqlite/pgsql/oracle等host
:数据库主机port
:数据库端口user
:数据库连接用户名pass
:数据库连接密码name
:需要连接的数据库名timezone
:数据库时区,设置为Asia/Shanghai
或者Local
,不设置的话会出现时间转换混乱debug
:是否开启调试,学习及开发阶段可开启调试,查看数据库操作相关信息输出
以上为连接数据库最简单的配置。如果需要进行更复杂的配置可查阅官方文档ORM使用配置
上述配置可以简化为一个link
,格式为type:user:password@tcp(host:prot)/dbname?param1=value1&..
database:
debug: true
link: "mysql:root:root@tcp(127.0.0.1:3306)/goframe?loc=Local&parseTime=true"
123
或者也可以保留上述配置,写为
database:
type: "mysql"
host: "127.0.0.1"
port: "3306"
user: "root"
pass: "root"
name: "goframe"
timezone: "Local"
debug: true
link: "mysql:root:root@tcp(127.0.0.1:3306)/goframe?loc=Local&parseTime=true"
12345678910
这样的写法使用的是link
,其他的单项配置不会生效。
驱动添加与导入
在main.go
中进行MySQL驱动初始化导入
import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
)
123
在go.mod
中添加驱动库与版本
require (
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.5.3
github.com/gogf/gf/v2 v2.5.3
)
1234
在命令行中进行依赖更新
go mod tidy
1
等等下载更新完成即可。
至此,在GoFrame中使用数据库的准备工作才准备完毕,正式进入数据库操作部分。
数据库基本操作
github.com/gogf/gf/v2/frame/g"
包里的Model
函数返回一个gdb.Model
对象,提供了一系列对数据库的操作。Model
函数接收一个参数,为数据表名:
md := g.Model("book")
1
返回一个与表book
关联的Model
查询数据
One/All/Count/Value/Array/Fields
查询数据库中一条数据
md := g.Model("book")
bk, err := md.One()
if err == nil {
req.Response.WriteJson(bk)
}
//SELECT * FROM `book` LIMIT 1
123456
返回数据库中第一条数据。查询成功返回的数据为map[string]*gvar.Var
类型,所以可以直接访问里面的每一字段:
req.Response.WriteJson(bk["name"]) // 返回结果中"name"字段
1
可以用gvar.Var
的方法对字符进行类型转换,转为需要的类型
bk["name"].String() // 转为string类型
bk["price"].Float32() // 转为float32类型
12
指定查询字段
bk, err := md.Fields("name, price").One() //只查询name price两个字符
// 也可以写为
bk, err := md.Fields("name", "price").One()
123
查询多条数据
md := g.Model("book")
bk, err := md.All()
12
该方法以切片返回数据表中所有数据,可以进行循环操作每一条数据
for _, v := range bk {
req.Response.Writeln(v)
}
123
查询数据数量
md := g.Model("book")
count, err := md.Count()
12
查询一条数据指定字段
md := g.Model("book")
name, err := md.Value("name")
12
查询指定列数据
md := g.Model("book")
name, err := md.Array("name")
12
Max/Min/Sum/Avg
GoFrame提供了最大最小值、求和、平均等方法
md := g.Model("book")
max, err := md.Max("price")
min, err := md.Min("price")
sum, err := md.Sum("price")
avg, err := md.Avg("price")
123456
Where/Where*/WhereOr/WhereOr*
使用?占位 和gorom一样 查询数据时可以通过Where
方法指定条件,如果有多个Where
,则多个条件之间会用AND
连接
本身查询 one,all的api 也接口也接收查询语句作为参数
等于
默认情况下条件会用=连接
md := g.Model("book")
books, err := md.Where("id", 1).All()
12
不等
如果是不等关系,需要在字段后面加上不等符号
md := g.Model("book")
books, err := md.Where("id>", 1).All()
12
多个条件叠加
有多个条件时可以多个Where
进行链式调用,条件会用AND
连接。
md := g.Model("book")
books, err := md.Where("id>=?", 2).Where("id<?", 4).All()
12
Where系列方法
方法 | 生成的SQL条件表达式 |
---|---|
WhereLT(column, value) | column < value |
WhereLTE(column, value) | column <= value |
WhereGT(column, value) | column > value |
WhereGTE(column, value) | column >= value |
WhereBetween(column, min, max) | column BETWEEN min AND max |
WhereNotBetween(column, min, max) | column NOT BETWEEN min AND max |
WhereLike(column, like) | column LIKE like |
WhereIn(column, in) | column IN (in) |
WhereNotIn(column, in) | column NOT IN (in) |
WhereNot(column, value) | column != value |
WhereNull(columns1, columns2… ) | columns1 IS NULL AND columns2 IS NULL… |
WhereNotNull(columns1, columns2… ) | columns1 IS NOT NULL AND columns2 IS NOT NULL … |
使用示例:
md := g.Model("book")
books, err := md.WhereIn("id", g.Array{1, 2, 3}).WhereLike("name", "%数据%").All()
// 生成如下SQL
// SELECT * FROM `book` WHERE (`id` IN (1,2,3)) AND (`name` LIKE '%数据%')
12345
以上方法如果链式调用会生成以AND
连接的条件,如果需要生成以OR
连接的条件,则需要用到下列方法:
记不住这些就只需要记得where(“条件语句 ?”,“值”)
WhereOr系列方法
方法 | 生成的SQL条件表达式 |
---|---|
WhereOrLT(column, value) | OR (column < value) |
WhereOrLTE(column, value) | OR (column <= value) |
WhereOrGT(column, value) | OR (column > value) |
WhereOrGTE(column, value) | OR (column >= value) |
WhereOrBetween(column, min, max) | OR (column BETWEEN min AND max) |
WhereOrNotBetween(column, min, max) | OR (column NOT BETWEEN min AND max) |
WhereOrLike(column, like) | OR (column LIKE like) |
WhereOrIn(column, in) | OR (column IN (in)) |
WhereOrNotIn(column, in) | OR (column NOT IN (in)) |
WhereOrNot(column, value) | OR (column != value) |
WhereOrNull(columns1, columns2… ) | OR (columns1 IS NULL AND columns2 IS NULL…) |
WhereOrNotNull(columns1, columns2… ) | OR (columns1 IS NOT NULL AND columns2 IS NOT NULL …) |
WhereOr(column, value) | OR (column = value) |
示例:
md := g.Model("book")
books, err := md.WhereIn("id", g.Array{1, 2, 3}).WhereOrLike("name", "%数据%").All()
// 生成如下SQL
// SELECT * FROM `book` WHERE (`id` IN (1,2,3)) OR (`name` LIKE '%数据%')
1234
Group/Order/Order*
按字段分组
md := g.Model("book")
books, err := md.Group("name").All()
12
按字段排序
md := g.Model("book")
books, err := md.Order("price", "DESC").All()
// 多字段排序
books, err := md.Order("price", "DESC").Order("id", "ASC").All()
// 排序封装方法
books, err := md.OrderDesc("price").OrderAsc("id").All()
123456
Scan
One
和All
返回的数据为Map
或者Map
切片,在实际使用当中查询到的数据可能需要转换为特定的数据结构方便使用。
Scan
方法可以将查询到的数据转为自定义结构体或结构体数组。该方法使用方式非常灵活,示例中只演示推荐写法。
查询数据转为自定义结构体
type Book struct {
Id uint
Name string
Author string
Price float64
PublishTime *gtime.Time
}
var book *Book
//查询结果转换为提前定义好的model
md := g.Model("book")
err := md.Scan(&book)
123456789101112
Scan
会将数据库字段下划线命名对应到结构体中相应的驼峰命名上,如果对应不上,则该成员为nil
或者零值。如果结构体中成员名称与数据表中字段不对应,可以用orm:
标签来指定对应字段
type Book struct {
BookId uint `orm:"id" `
BookName string `orm:"name"`
BookAuthor string `orm:"author"`
BookPrice float64 `orm:"price"`
PubTime *gtime.Time `orm:"publish_time"`
}
var book *Book
md := g.Model("book")
err := md.Scan(&book)
123456789101112
结构体数组
Scan
方法可以查询单独结构体,如上,也可以查询一个结构体数组,只需要将结构体指针改为结构体切片传入即可
type Book struct {
Id uint
Name string
Author string
Price float64
PublishTime *gtime.Time
}
var book []Book
md := g.Model("book")
err := md.Scan(&book)
123456789101112
查询结果为一个由Book
组成的结构体数组,存放多条数据。
查询部分暂时就先了解这些,实际上只要SQL熟悉的话每种查询基本上都能找到对应的方法来实现。更复杂的查询见官方文档ORM查询
查询结果为空判断
All
md := g.Model("book")
books, _ := md.All()
if len(books) == 0 {
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
// 或者
if books.IsEmpty() {
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
123456789
One
md := g.Model("book")
book, _ := md.Where("id", 100).One()
if len(book) == 0 {
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
// 或者
if book.IsEmpty() {
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
123456789
Value
md := g.Model("book")
name, _ := md.Where("id", 10).Value("name")
if name.IsEmpty() {
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
12345
Array
md := g.Model("book")
names, _ := md.WhereLT("id", 10).Array("name")
if len(names) == 0{
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
12345
Scan结构体对象
var book *Book
md := g.Model("book")
md.Scan(&book)
if book == nil {
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
md.Save(data)
1234567
Scan结构体数组
var books []Book
md := g.Model("book")
md.Scan(&books)
if len(books) == 0 {
g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
123456
分页
GoFrame中提供了Page
方法可以很方便实现分页查询,只需提供页数和每页数据数量即可。
md := g.Model("book")
books, err := md.Page(1, 5).All()
12
也有Limit
方法可以用来限制查询条数以及自定义起始位置与数据限制
md := g.Model("book")
// 限制条数
books, err := md.Limit(5).All()
// 指定起始位置与限制条数 (currentpage-1)*pagesize,pagesize
books, err := md.Limit(3, 5).All()
12345
原生sql 查询
func GetBookRaw() any {
if result, totalCount, err := md.Raw("select * from book limit 10").AllAndCount(false); err == nil {
fmt.Println("查询语句得到%d条记录,总数", totalCount)
return result
}
return nil
}
1234567
多表联查
xxxJOIN(“连接表”,“字段表达式”)
one, err := md.InnerJoin("user").Where("user.id = ?", id).One()
// SELECT * FROM `book` INNER JOIN `user` WHERE user.id = 1 LIMIT 1
12
//mb 不能重复用
md := g.DB("default").Model("book")
//SELECT * FROM `book` INNER JOIN `user` ON (`book`.`nickname`=`user`.`nickname`) WHERE `book`.id = 3 LIMIT 1
one, err := md.LeftJoin("user", "user.nickname=book.author").Where("id ", id).One()
//one, err := md.InnerJoin("user", "book.author = user.nickname").Where("user.id = ?", id).One()
12345
如果俩个字段名字一样 可以直接使用下面的api
one, err := md.LeftJoinOnField("user", "id").Where("id ", id).One()
// SELECT * FROM `book` LEFT JOIN `user` ON (`book`.`id`=`user`.`id`) WHERE `book`.`id`=1 LIMIT 1
12
插入数据
Insert/Replace/Save
这三个方法都可以向数据库中写入一条或者多条数据,区别在于当数据中主键字段在数据库中已经存在时,处理方式不同:
方法 | 主键在数据库中已存在时 |
---|---|
Insert | 报错,主键冲突 |
Repalce | 用提供的数据替换已存在同主键的数据 |
Save | 用提供的数据更新已存在的同主键数据 |
Replace
Replace
方法通常用于完全替换一个存在的记录。如果指定的记录在数据库中存在,它会更新那条记录的所有字段,即使某些字段在 data
中没有被提供(这些未提供的字段通常会被设置为默认值或空值)。如果记录不存在,Replace
会插入一条新的记录。在某些数据库(如 MySQL)中,REPLACE
实际上是一个删除旧记录再插入新记录的操作。这意味着它可能会影响到像是自增主键这样的字段。
Save
Save
方法更通用,它通常在数据库中查找记录是否存在,如果存在则更新它,如果不存在则插入新记录。与 Replace
不同,Save
通常只会更新提供了数据的字段,不会触及其他未在 data
中提供的字段。Save
方法通常保留记录的完整性,不会像 Replace
那样删除再重建记录,因此在使用自增主键的情况下通常更为安全。
写入单条数据
md := g.Model("book")
data := g.Map{
"id": 8,
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69,
"publish_time": "2023-10-10",
}
// Insert
result, err := md.Insert(data)
// Replace
result, err := md.Replace(data)
// Save
result, err := md.Save(data)
1234567891011121314
以上方法也可配合Data
使用(先绑定数据 和直接操作数据的区别)
// Insert
result, err := md.Data(data).Insert()
// Replace
result, err := md.Data(data).Replace()
// Save
result, err := md.Data(data).Save()
123456
除了使用Map
类型之外,还可以用结构体。结构体成员名称与数据表字段名称不对应时,用orm
标签指定
type Book struct {
Id uint
Name string
Author string
Price float64
PubTime *gtime.Time `orm:"publish_time"`
}
md := g.Model("book")
data := Book{
Id: 8,
Name: "Linux驱动开发入门与实践",
Author: "郑强",
Price: 69.3,
PubTime: gtime.New("2023-10-10"),
}
result, err := md.Data(data).Save()
123456789101112131415161718
批量写入数据
上述方法也可以批量写入数据
data := g.List{
g.Map{
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69.3,
"publish_time": gtime.New("2023-10-10"),
},
g.Map{
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69.3,
"publish_time": gtime.New("2023-10-10"),
},
g.Map{
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69.3,
"publish_time": gtime.New("2023-10-10"),
},
}
result, err := md.Data(data).Save()
12345678910111213141516171819202122
如果使用的是结构体,将g.List
改为g.Array
或者g.Slice
InsertAndGetId
写入数据并返回自增ID
data := g.Map{
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69.3,
"publish_time": gtime.New("2023-10-10"),
}
result, err := md.Data(data).InsertAndGetId()
12345678
gdb.Raw
对于有的字段,可能需要调用SQL里面的操作来获得结果,例如,publish_time
字段可以用SQL中的CURRENT_DATE()
来获取当前日期,这时就需要用到Raw
:
data := g.Map{
"name": "Linux驱动开发入门与实践",
"author": "郑强",
"price": 69.3,
"publish_time": gdb.Raw("CURRENT_DATE()"),
}
result, err := md.Data(data).InsertAndGetId()
12345678
更新数据
Update
data := g.Map{
"author": "郑强强",
"price": 69.333,
}
result, err := md.Where("author", "郑强").Update(data)
123456
也可以配合Data
使用
data := g.Map{
"author": "郑强强",
"price": 69.333,
}
result, err := md.Where("author", "郑强").Data(data).Update()
123456
Increment/Decrement
用来给指定字段增加/减少指定值
result, err := md.WhereBetween("id", 7, 10).Increment("price", 2.5)
result, err := md.WhereBetween("id", 7, 10).Decrement("price", 1.5)
12
删除数据
result, err := md.WhereGT("id", 10).Delete()
1
时间维护与软删除
在实际应用当中,数据表中通常会有三个时间字段:创建时间、更新时间、删除时间。GoFrame支持这三个时间字段的自动填充,这三个字段支持的类型为DATE
、DATETIME
、TIMESTAMP
。
创建时间:默认为created_at
更新时间:默认为updated_at
删除时间:默认为deleted_at
,数据软删除时使用
如果不想使用默认名称,需要自行修改,可以在配置文件里数据库配置时修改,方式如下:
database:
....
createdAt: "create_time"
updatedAt: "update_time"
deletedAt: "delete_time"
12345
软删除(逻辑删除)
软删除并不是真正从数据库中把记录删除,而是通过特定的标记在查询时过滤掉这些数据,使这些数据在页面上看不到,但实际上在数据库中仍然存在。通常用于一些需要历史追踪而不能真正删除的数据。
当数据表中有deleted_at
字段时,使用Delete
方法时不会物理删除数据,只是更新deleted_at
字段的值。查询数据时,会自动加上WHERE `deleted_at` IS NULL这一条件,过滤掉已被“删除”的数据。
如果需要查询所有数据,需要使用Unscoped
方法
ls, _ := md.Unscoped().All()
1
事务处理
常规写法
tx, err := g.DB().Begin(ctx)
if err == nil {
_, err := tx.Model("book").Data(data).Save()
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}
12345678910
闭包写法(框架建议写法)
g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
_, err := tx.Model("book").Ctx(ctx).Save(data)
return err
})
1234
原生SQL的使用
由Model
提供的方法能组合出绝大多数使用场景所需要的数据操作,但如果需要的操作过于复杂,可能就没法通过已有的方法组合出来,就需要使用写SQL来实现
查询
db := g.DB()
books, err := db.Query(ctx, "SELECT * FROM `book` WHERE `id` > ? AND `id` < ?", g.Array{3, 7})
12
新增数据
db := g.DB()
sql := "INSERT INTO `book` (`name`, `author`, `price`) VALUES (?, ?, ?)"
data := g.Array{"Go语言从入门到精通", "Go语言研讨组", 99.98}
result, err := db.Exec(ctx, sql, data)
1234
更多操作查看官方文档ORM方法操作(原生)
DAO自动生成与使用
数据库相关的操作与数据结构放在dao
与model
中,在GoFrame中,dao
与model
的内容可以自动生成。生成步骤如下:
配置dao
hack/comfig.yaml
gfcli:
gen:
dao:
link: "mysql:root:root@tcp(127.0.0.1:3306)/goframe"
tables: "book, user, dept, emp, hobby"
jsonCase: "Snake"
123456
link: 数据库连接urltables: 需要生成dao及model的数据表,多个表用逗号隔开jsonCase: entity成员转为json时的转换方式,”Snake”为把驼峰转为下划线
以上为最简单配置,更多配置见官方文档代码生成/数据规范 gen dao章节
进入配置文件目录
创建internal文件夹
在命令行中执行如下命令
gf gen dao
1
该命令会生成dao
以及model
下各个表对应的结构与代码。
接下来使用各个表对应的Model对象时,不再用g.Model
获取,而是用下面的的方式:
func (c *UserController) UserAll(r *ghttp.Request) {
// TODO: implement login logic
md := dao.User.Ctx(r.Context()) // 使用 r.Context() 获取上下文
if books, err := md.All(); err != nil {
r.Response.WriteJson(err.Error())
} else {
r.Response.WriteJson(books)
}
}
12345678910
该Model对象可以多次叠加查询条件:
md := dao.Book.Ctx(ctx)
md = md.WhereGT("id", 3)
md = md.WhereLT("id", 6)
books, err := md.All()
// 以上代码相当于
books, err := dao.Book.Ctx(ctx).WhereGT("id", 3).WhereLT("id", 6).All()
1234567
字段过滤
使用结构体数据进行创建或更新数据时,尤其是在更新数据的时候,有些字段可能不需要更新,因此对应的字段就不进行赋值,例如以下代码
type Book struct {
Id uint
Name string
Author string
Price float64
PubTime *gtime.Time `orm:"publish_time"`
}
data := Book{
Name: "Linux驱动开发入门与实践",
PubTime: gtime.New("2023-10-11"),
}
//没有id 不能使用save 更新
_, err = dao.Book.Ctx(ctx).Where("id", 13).Data(data).Update()
1234567891011121314
直接这样更新,则id
、author
、price
也会被对应类型的零值更新,分别被更新为0、“”、0
要解决这样的问题,有以下几种解决方案:
用Fields指定需要更新的字段
dao.Book.Ctx(ctx).Fields("name", "publish_time").Where("id", 13).Data(data).Update()
1
用FieldsEx排除不需要更新的字段
dao.Book.Ctx(ctx).FieldsEx("id,author,price").Where("id", 13).Data(data).Update()
1
用OmitEmpty过滤空值
data := Book{
Name: "Linux驱动开发入门与实践",
Price: 0,
PubTime: nil,
}
dao.Book.Ctx(ctx).Where("id", 13).OmitEmpty().Data(data).Update()
123456
用这种方法如上数据中,0和nil
也会被忽略,没法更新对应字段的值。
使用do对象进行字段过滤
使用gf gen dao
时,每个表会生成一个对应的do
对象,使用do
对象作为参数传递,将会自动过滤空值
data := do.Book{
Name: "Linux驱动开发入门与实践",
Price: 0,
PublishTime: nil,
}
dao.Book.Ctx(ctx).Where("id", 13).Data(data).Update()
1234567
使用这种方法,非nil
的零值都可以更新。
do
对象也可以用于传递查询条件, 也会自动过滤空值
where := do.Book{
Author: "郑强",
Id: 13,
PublishTime: nil,
}
books, err := dao.Book.Ctx(ctx).Where(where).All()
// 相当于
books, err := dao.Book.Ctx(ctx).Where("id", 13).Where("author", "郑强").All()
123456789
关联查询
多表数据联查时可以用连接,但是数据量大时连接效率不高,GoFrame中提供了模型关联查询,可以简化一些多表联查操作。
以dept
、emp
、hobby
三个表为例,每个部门可以有多个员工,每个员工只有一个部门,每个员工对应一条爱好。
查询所有员工,并关联查询出其所在部门
修改entity.Emp
,加入关联信息
internal/model/entity/emp.go
type Emp struct {
Id uint `json:"id" ` // ID
DeptId uint `json:"dept_id" ` // 所属部门
Name string `json:"name" ` // 姓名
Gender int `json:"gender" ` // 性别: 0=男 1=女
Phone string `json:"phone" ` // 联系电话
Email string `json:"email" ` // 邮箱
Avatar string `json:"avatar" ` // 照片
Dept *Dept `orm:"with:id=dept_id" json:"dept"`
}
12345678910
使用With
指定关联模型查询
var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).With(entity.Dept{}).Scan(&emps)
12
不用With
指定关联的话,查询出的结果中Dept
为nil
查询所有员工,并关联查询出部门与爱好
修改entity.Emp
,加入关联信息
internal/model/entity/emp.go
type Emp struct {
Id uint `json:"id" ` // ID
DeptId uint `json:"dept_id" ` // 所属部门
Name string `json:"name" ` // 姓名
Gender int `json:"gender" ` // 性别: 0=男 1=女
Phone string `json:"phone" ` // 联系电话
Email string `json:"email" ` // 邮箱
Avatar string `json:"avatar" ` // 照片
Dept *Dept `orm:"with:id=dept_id" json:"dept"`
Hobby *Hobby `orm:"with:emp_id=id" json:"hobby"`
}
123456789101112
使用With
指定需要关联的内容
var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).With(entity.Dept{}, entity.Hobby{}).Where("dept_id", 101).Scan(&emps)
12
也可以用WithAll
关联所有
var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).WithAll().Where("dept_id", 101).Scan(&emps)
12
查询部门,关联查询出每个部门的员工
修改entity.Dept
,加入关联信息
internal/model/entity/dept.go
// Dept is the golang structure for table dept.
type Dept struct {
Id uint `json:"id" ` // ID
Pid uint `json:"pid" ` // 上级部门ID
Name string `json:"name" ` // 部门名称
Leader string `json:"leader" ` // 部门领导
Phone string `json:"phone" ` // 联系电话
Emps []*Emp `orm:"with:dept_id=id" json:"emps"`
}
12345678910
查询
var depts []*entity.Dept
err = dao.Dept.Ctx(ctx).With(entity.Emp{}).Scan(&depts)
12
上述关联查询直接在实体类里面修改,但实体类里的内容是用工具自动生成的,一般情况下不要修改。所以在进行关联查询时,需要重新自定义结构体,只需要保留需要查询的字段即可(用于关联的字段必须存在)
type MyDept struct {
g.Meta `orm:"table:dept"`
Id uint `json:"id" ` // ID
Name string `json:"name" ` // 部门名称
Leader string `json:"leader" ` // 部门领导
Phone string `json:"phone" ` // 联系电话
}
type MyEmp struct {
g.Meta `orm:"table:emp"`
Id uint `json:"id" ` // ID
DeptId uint `json:"dept_id" ` // 所属部门
Name string `json:"name" ` // 姓名
Phone string `json:"phone" ` // 联系电话
Dept *MyDept `orm:"with:id=dept_id" json:"dept"` //填充的结构体 tag 标识对应的主键id
}
var emps []*MyEmp
//先查询返回的员工表
//然后根据员工表集合查询 批量查询
err = dao.Emp.Ctx(ctx).With(MyDept{}).Scan(&emps)
// SELECT `id`,`dept_id`,`name`,`phone` FROM `emp`
//SELECT id,name,leader,phone FROM dept WHERE id IN(100,101,102,103,104,106,107)
1234567891011121314151617181920212223242526
自定义结构体时,需要用g.Meta
及orm
标签指定对应的数据表
模板引擎
前面我们提过可以用下面几个方法返回模板内容
func (r *Response) WriteTpl(tpl string, params ...gview.Params) error
func (r *Response) WriteTplDefault(params ...gview.Params) error
func (r *Response) WriteTplContent(content string, params ...gview.Params) error
123
默认情况下模板文件目录是resource/template
WriteTpl
解析并返回模板文件内容,文件为相对于resource/template
的路径WriteTplDefault
解析并返回默认模板,为resource/template/index.html
WriteTplContent
解析并返回模板字符串
常用的为WriteTpl
,其他两个方法简单了解即可
func (c *Controller) Tpl(req *ghttp.Request) {
req.Response.WriteTplDefault() // 解析并返回默认模板文件内容
// 解析并返回模板字符串
req.Response.WriteTplContent("<h1>你好, {{.name}} 欢迎学习{{.lang}}</h1>", g.Map{"name": "王道长", "lang": "GoFrame"})
}
12345
简单使用示例
resource/template/hello/index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<h1>你好, {{.name}}</h1>
<h2>欢迎来到{{.lesson}}的学习课程</h2>
<p>本课程共{{.num}}小节,现在学习的是{{.what}}</p>
</div>
</body>
</html>
1234567891011121314151617
controller
func (c *Controller) Tpl(req *ghttp.Request) {
data := g.Map{
"name": "王也道长",
"lesson": "GoFrame入门课程",
"num": 5,
"what": "模板引擎使用示例",
}
req.Response.WriteTpl("hello/index.html", data)
}
12345678910
模板配置
模板使用当中一般情况使用默认配置即可,如果需要修改配置,则在manifest/config/config.yaml
中进行修改
viewer: # 模板配置
paths: ["resource/template", "/www/template"] # 模板路径配置,可以配置多个路径
defaultFile: "index.hmtl" # WriteTplDefault解析的文件
delimiters: ["${", "}"] # 模板引擎变量分隔符,默认为["{{", "}}"]
1234
静态资源
静态资源不属于模板引擎的内容,但在模板文件中也有需要用到静态资源的地方,因此进行一下补充。
静态资源一般指的是js/css/image文件或者静态HTML文件,在GoFrame的项目目录中,这些文件放在resource/public
下,之后还需要开启静态资源服务才能在模板文件中对这些资源进行引用。开启方式有两种
配置文件
manifest/config/config.yaml
server:
serverRoot: "resource/public"
indexFolder: true # 这个可以不用配置,放在这里了解一下
1234
用代码开启
internal/cmd/cmd.go
s := g.Server()
s.SetServerRoot("resource/public")
s.SetIndexFolder(true)
123
serverRoot
配置了静态资源根目录resource/public
,对静态资源的引用url以resource/public
为根目录
例如,在resource/public/resource/css
中放置了一人bootstrap.css
文件,引用时写为
<link rel="stylesheet" href="/resource/css/bootstrap.css">
1
条件判断
在模板中可以进行条件判断,根据条件是否满足来显示不同内容,语法如下:
{{if .condition}}
条件满足时显示内容
{{else}}
条件不满足时显示内容
{{end}}
12345
可以嵌套写,也可以写多个{{else if .condition}}
当.condition
为空值,即0
、""
、nil
这类值时,条件判断为假,其他值均为真(条件满足)。
用法示例:
resource/template/hello/index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/resource/css/bootstrap.css">
<title>Document</title>
</head>
<body>
{{if .name}}
<h1>name的值: {{.name}}</h1>
{{else}}
<h1>name的值为false/""/0/nil等</h1>
{{end}}
</body>
</html>
123456789101112131415161718
controller
func (c *Controller) Tpl(req *ghttp.Request) {
data := g.Map{
"name": "王也道长",
"lesson": "GoFrame入门课程",
"num": 5,
"what": "模板引擎使用示例",
}
req.Response.WriteTpl("hello/index.html", data)
}
12345678910
大于小于等于判断
在模板中无法直接使用>
、<
、==
等符号进行关系判断,因此需要使用条件函数
函数 | 对应符号 |
---|---|
eq | == |
ne | != |
lt | < |
le | <= |
gt | > |
ge | >= |
使用示例
{{if eq 5 .num}}
<h1>num == 5</h1>
{{else if lt 5 .num}}
<h1>num > 5</h1>
{{else}}
<h1>num < 5</h1>
{{end}}
1234567
上述函数还有一些拓展用法,这里只简单介绍基础用法
逻辑判断
模板语言中可以用and
、or
、not
进行逻辑运算
{{if and (gt .num 0) (lt .num 5)}} if num > 0 && num < 5
{{if or (eq .num 0) (eq .num 5)}} if num == 0 || num == 5
{{if not (eq .num 0)}} if num != 0
123
循环
range ... end
循环切片
controller/hello.go
data := g.Map{
"slice": g.Array{1, 2, 3, "张楚岚", "诸葛青"},
}
req.Response.WriteTpl("hello/index.html", data)
12345
index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/resource/css/bootstrap.css">
<title>Document</title>
</head>
<body>
{{range .slice}}
<span>{{.}}</span>
{{end}}
{{range $index, $value := .slice}}
<p>index = {{$index}}, value = {{$value}}</p>
{{end}}
</body>
</html>
123456789101112131415161718192021
map数据
controller/hello.go
func (c *Controller) Tpl(req *ghttp.Request) {
data := g.Map{
"mp": g.Map{
"name": "冯宝宝",
"gender": "女",
"age": 100,
"hobby": "埋人",
},
}
req.Response.WriteTpl("hello/index.html", data)
}
123456789101112
index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/resource/css/bootstrap.css">
<title>Document</title>
</head>
<body>
<div class="container">
<p>姓名:{{.mp.name}}</p>
<p>性别:{{.mp.gender}}</p>
<p>年龄:{{.mp.age}}</p>
<p>爱好:{{.mp.hobby}}</p>
{{range .mp}}
<p>{{.}}</p>
{{end}}
{{range $key, $value := .mp}}
<p>{{$key}}: {{$value}}</p>
{{end}}
</div>
</body>
</html>
123456789101112131415161718192021222324252627282930
上传与下载
文件上传
文件上传可以通过表单上传,也可以通过Ajax上传,GoFrame框架后端处理都是一样的,所以只演示一下表单上传。
单文件上传
html/upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传文件</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="ufile"> <br>
<input type="file" name="ufiles" multiple> <br>
<input type="submit" value="上传">
</form>
</body>
</html>
1234567891011121314151617
controller/hello.go
func (c *Controller) Upload(req *ghttp.Request) {
file := req.GetUploadFile("ufile")
if file != nil {
file.Filename = "20231001.png" // 可以根据需要给文件重命名
name, err := file.Save("./upload")
if err == nil {
req.Response.Writeln(name)
}
}
}
12345678910
多文件上传
func (c *Controller) Upload(req *ghttp.Request) {
files := req.GetUploadFiles("ufiles")
if files != nil {
names, err := files.Save("./upload")
if err == nil {
req.Response.Writeln(names)
}
}
}
123456789
除了从请求中获取上传文件以外,如果用api规范路由,还可以用如下方式获取上传文件
type UploadReq struct {
g.Meta `path:"/upload" method:"post"`
Ufile ghttp.UploadFile `json:"ufile"`
UFiles ghttp.UploadFiles `json:"ufiles"`
}
1234567
使用这种方式,如果文件允许为空, 则可能会发生转换错误。
文件上传应用实例
在静态资源目录新建upload
文件夹用于存放上传文件,示例中为resource/public/upload
,绑定的静态目录为resource/public
,因此可以用/upload/<filename>
的形式访问指定文件将上传文件名称修改为对应文件的哈希值,以防上传同名文件覆盖返回文件的URL
func (c *Controller) Upload(ctx context.Context, r *api.UploadReq) (res *api.UploadRes, err error) {
req := g.RequestFromCtx(ctx)
file := req.GetUploadFile("ufile")
if file != nil {
var md5str string
md5str, err = gmd5.Encrypt(file)
if err != nil {
return
}
file.Filename = md5str + path.Ext(file.Filename)
name, err := file.Save("resource/public/upload")
if err == nil {
res = &api.UploadRes{
Data: "/upload/" + name,
}
}
}
return
}
12345678910111213141516171819
文件下载
ServeFile
ServeFile
向客户端返回一个文件内容,如果是文本或者图片,将会直接展示,不能直接在浏览器中展示的将进行下载
func (c *Controller) Download(req *ghttp.Request) {
req.Response.ServeFile("upload/1.png")
}
123
ServeFileDownload
该方法直接引导客户端进行下载,并且可以给下载文件重命名
func (c *Controller) Download(req *ghttp.Request) {
req.Response.ServeFileDownload("upload/1.png", "download.png")
}
123
上传限制
如果需要限制单次上传文件大小,可以用clientMaxBodySize
配置。如果完全不需要限制,直接设为0即可
config.yaml
server:
clientMaxBodySize: "0"
12
数据校验
在使用中,经常需要验证前端提交过来的数据是否符合规则,比如非空、长度限制、是否为数字等一系列验证。在GoFrame中,基本上都不用手动写验证规则,框架里已经提供了很多内置的验证规则可以用来验证数据。验证规则详细内容见官方文档数据校验/校验规则
单个规则/错误提示信息
func (c *Controller) Valid(ctx context.Context, rq *api.ValidReq) (rs *api.ValidRes, err error) {
type Data struct {
Name g.Map `v:"required#name不能为空"`
Age int `v:"required"`
Phone string `v:"required"`
}
data := Data{}
err = g.Validator().Bail().Data(data).Run(ctx)
rs = &api.ValidRes{Data: data}
return
}
12345678910111213
多个规则
func (c *Controller) Valid(ctx context.Context, rq *api.ValidReq) (rs *api.ValidRes, err error) {
type Data struct {
Name string
Age string `v:"required|integer|min:1#age不能为空|age必须为整数|age不能小于1"`
Phone string
}
data := Data{Age: "1.1"}
err = g.Validator().Bail().Data(data).Run(ctx)
rs = &api.ValidRes{Data: data}
return
}
12345678910111213
使用Map指定校验规则
func (c *Controller) Valid(ctx context.Context, rq *api.ValidReq) (rs *api.ValidRes, err error) {
type Data struct {
Name string
Age int
Phone string
}
rules := map[string]string{
"Name": "required|length:6,16",
"Age": "between:18,30",
"Phone": "phone",
}
message := map[string]interface{}{
"Name": map[string]string{
"required": "Name不能为空",
"length": "长度只能为{min}到{max}个字符",
},
"Age": "年龄只能为18到30岁",
}
data := Data{Phone: "123"}
err = g.Validator().Rules(rules).Messages(message).Data(data).Run(ctx)
rs = &api.ValidRes{Data: data}
return
}
12345678910111213141516171819202122232425
规范路由API数据校验
如果输入数据直接在API里定义了结构,可直接将校验规则写上,在请求时会自动校验,不需要再手动调用校验函数。
api/hello.go
type ValidReq struct {
g.Meta `path:"/valid" method:"all"`
Name string `v:"required|length:6,16"`
Age int `v:"required"`
Phone string `v:"phone"`
}
type ValidRes struct {
Data interface{} `json:"data"`
}
1234567891011
controller/hello.go
func (c *Controller) Valid(ctx context.Context, rq *api.ValidReq) (rs *api.ValidRes, err error) {
return
}
123
Cookie/Session
Cookie是保存在浏览器的一些数据,在请求的时候会放在请求头当中一同发送,通常用来保存sessionid、token等一些数据。
func (c *Controller) Cookie(req *ghttp.Request) {
req.Cookie.Set("id", "kslfjojklcjkldjfsie")
req.Cookie.Set("user_name", "诸葛青")
name := req.Cookie.Get("user_name")
req.Response.Writeln("name from cookie: " + name.String())
req.Cookie.Remove("id")
}
123456789
Session机制用于判断请求由哪一用户发起,Session数据保存在服务器。
以前常用于保存登录数据,进行登录验证,不过现在只是有些比较小的,前后端不分离的项目还在使用。
func (c *Controller) Session(req *ghttp.Request) {
op := req.GetQuery("op").String()
if op == "set" {
req.Session.Set("user", g.Map{"name": "张三", "id": 18})
} else if op == "get" {
req.Response.Writeln(req.Session.Get("user"))
} else if op == "rm" {
req.Session.Remove("user")
}
}
12345678910
golang-jwt
前后端分离的项目更常用的登录验证是JWT(JSON web token)
。GoFrame中没有提供相关生成与验证,需要添加第三方库,例如golang-jwt
简单使用方式如下:
添加
go get -u github.com/golang-jwt/jwt/v5
1
导入
import "github.com/golang-jwt/jwt/v5"
1
生成token
func (c *Controller) Jwt(req *ghttp.Request) {
type UserClaims struct {
UserID uint
UserName string
jwt.RegisteredClaims
}
const key = "arandomstring"
claim := UserClaims{
UserID: 1011,
UserName: "张之维",
RegisteredClaims: jwt.RegisteredClaims{
Subject: "张之维",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 10)),
},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claim).SignedString([]byte(key))
if err == nil {
req.Response.Writeln(token)
} else {
req.Response.Writeln(err)
}
}
123456789101112131415161718192021222324
token验证
func (c *Controller) Jwt(req *ghttp.Request) {
type UserClaims struct {
UserID uint
UserName string
jwt.RegisteredClaims
}
const key = "arandomstring"
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEwMTEsIlVzZXJOYW1lIjoi5byg5LmL57u0Iiwic3ViIjoi5byg5LmL57u0IiwiZXhwIjoxNjk4NjcxMjA3fQ.r11R1_WcDueBU52BoUjDS94jqemgrhU-V4WW7YSvXWE"
result, err := jwt.ParseWithClaims(token, &UserClaims{}, func(t *jwt.Token) (interface{}, error) {
return []byte(key), nil
})
if err == nil && result.Valid {
claim, ok := result.Claims.(*UserClaims)
if ok {
req.Response.Writeln("token验证成功")
req.Response.Writeln(claim)
}
req.Response.Writeln(result.Claims)
} else {
req.Response.Writeln(err)
}
}
12345678910111213141516171819202122232425
测试
func TestEncodeJWT(t *testing.T) {
type UserClaims struct {
UserID uint
UserName string
jwt.RegisteredClaims
}
claim := UserClaims{
UserID: 1011,
UserName: "张之维",
RegisteredClaims: jwt.RegisteredClaims{
Subject: "张之维",
//用于将time.Time类型的时间转换为JWT标准中使用的NumericDate类型。
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 70)),
},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claim).SignedString([]byte(key))
if err == nil {
t.Log(token)
} else {
t.Error("生成token失败")
}
}
func TestDecodeJWT(t *testing.T) {
type UserClaims struct {
UserID uint
UserName string
jwt.RegisteredClaims
}
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEwMTEsIlVzZXJOYW1lIjoi5byg5LmL57u0Iiwic3ViIjoi5byg5LmL57u0IiwiZXhwIjoxNzI0NjU0MDEzfQ.Whhmfv66Xvsou7BinU1l_by7331__4aETjUK2adeQO4"
// TODO 会自动校验是否过期
result, err := jwt.ParseWithClaims(token, &UserClaims{}, func(t *jwt.Token) (interface{}, error) {
return []byte(key), nil
})
if err == nil && result.Valid {
claim, ok := result.Claims.(*UserClaims)
if ok {
t.Log("token验证成功")
t.Log(claim)
}
subject, _ := result.Claims.GetSubject()
t.Log(subject)
} else {
t.Error("token验证失败")
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
jwt.RegisteredClaims
type RegisteredClaims struct {
// 发布者
Issuer string `json:"iss,omitempty"`
// token使用主体
Subject string `json:"sub,omitempty"`
//
Audience ClaimStrings `json:"aud,omitempty"`
// 失效时间
ExpiresAt *NumericDate `json:"exp,omitempty"`
// 生效时间
NotBefore *NumericDate `json:"nbf,omitempty"`
// 发布时间
IssuedAt *NumericDate `json:"iat,omitempty"`
// 可以唯一标识这一jwt的字符串,用来防止数据相似的jwt哈希碰撞
ID string `json:"jti,omitempty"`
}
12345678910111213141516171819202122
中间件
中间件(拦截器)是在请求与响应的过程中,拦截下请求与响应并做一些操作,常见的用途是在请求前进行鉴权,在请求后对响应数据进行包装等
中间件的定义与普通路由函数一样,只是其中需要用Next()
进行路由放行。
func MiddleWare(r *ghttp.Request) {
r.Middleware.Next()
}
123
前置中间件
后置中间件
在路由函数执行完成之后再进行操作的中间件称为后置中间件,例如对返回的数据格式进行统一封装的中间件:
中件间的定义实际上就是如下:
// 和gin一样 这里的AuthMiddleware就是一个中间件,
// 分为执行前和执行后两个阶段,执行前的阶段主要是对请求进行拦截,执行后的阶段主要是对响应进行处理。
// 可以对请求进行拦截,比如检查用户是否登录,如果未登录则跳转到登录页面。
type AuthMiddleware struct {
}
// 前置中间件
func Auth(r *ghttp.Request) {
user := r.Header.Get("Authorization")
if user == "" {
r.Response.WriteJson(g.Map{
"code": 401,
"msg": "Unauthorized",
})
log.Default().Printf("请求被拦截: %v")
//拦截请求
r.ExitAll()
}
r.Middleware.Next()
}
/*
*
后置中间件处理响应
*/
func ResponseHandler(r *ghttp.Request) {
/**
r 是一个 ghttp.Request 对象,表示当前的 HTTP 请求。
Middleware 是 ghttp.Request 对象的一个属性,用于管理中间件。
Next() 是 Middleware 的一个方法,用于调用下一个中间件或处理函数。
*/
r.Middleware.Next()
if r.Response.BufferLength() > 0 {
return
}
res := r.GetHandlerResponse()
r.Response.WriteJson(res)
// 这里可以对响应进行处理,比如添加响应头、修改响应内容等。
log.Default().Printf("后置请求处理: %v", res)
}
123456789101112131415161718192021222324252627282930313233343536373839404142
全局路由中进行绑定
s.BindMiddlewareDefault(middleware.Auth, middleware.ResponseHandler)
1
分组隔离的情况绑定中间件
func main() {
s := g.Server()
// 不需要身份验证的路由
s.Group("/", func(group *ghttp.RouterGroup) {
group.GET("/public", func(r *ghttp.Request) {
r.Response.WriteJson(g.Map{
"message": "This is a public endpoint.",
})
})
})
// 需要身份验证的路由
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Auth)
group.GET("/private", func(r *ghttp.Request) {
r.Response.WriteJson(g.Map{
"message": "This is a private endpoint.",
})
})
})
s.Run()
}
123456789101112131415161718192021222324
SetCtxVar/GetCtxVar
上下文处理 如果需要在一些请求流程中进行参数传递,可以用SetCtxVar/GetCtxVar
进行存取 也是携程隔离的
例如
func MiddleWare(r *ghttp.Request) {
r.SetCtxVar("UserName", "陆玲珑")
r.Middleware.Next()
}
1234
在具体路由函数中取用
UserName := r.GetCtxVar("UserName")
1
组件
数据结构
GoFrame中提供了一些常用的数据结构,如列表、队列、集合、Map等,详细内容见官方文档。
时间
当前时间
t := gtime.Now()
1
t := gtime.Date()
1
t := gtime.Datetime()
1
创建时间对象
t := gtime.New("2023-11-03 21:45:22")
1
参数可以是字符串、时间戳、时间对象等数据
t := gtime.NewFromStr("2023-11-03 21:50:25")
1
t, err := gtime.StrToTime("2023-11-03 21:50:25")
1
将字符串转为时间对象,具体支持的时间格式见文档时间管理/工具方法
设置时区
gtime.SetTimeZone("Asia/Tokyo")
t := gtime.Now()
12
时间戳
t1 := gtime.Timestamp()
t2 := gtime.TimestampMilli()
t3 := gtime.TimestampMicro()
t4 := gtime.TimestampNano()
1234
返回为int64
类型,也可以返回字符串类型
t1 := gtime.TimestampStr()
t2 := gtime.TimestampMilliStr()
t3 := gtime.TimestampMicroStr()
t4 := gtime.TimestampNanoStr()
1234
格式化日期数据
可以将日期格式化为指定的格式,具体格式化用到的符号见文档时间管理/时间格式
t := gtime.Now()
req.Response.Writeln(t.Format("Y-m-d H:i:s"))
req.Response.Writeln(t.Format("YmdHis"))
123
获取年月日时分秒
t := gtime.Now()
req.Response.Writeln(t.Year())
req.Response.Writeln(t.Month())
req.Response.Writeln(t.Day())
req.Response.Writeln(t.Hour())
req.Response.Writeln(t.Minute())
req.Response.Writeln(t.Second())
1234567
更多操作方法见文档时间管理/方法介绍
随机数
随机整数
n := grand.Intn(100)
1
返回0 <= n < 100的随机数
n := grand.N(100, 999)
1
返回100 <= n <= 999的随机数
随机字符串
s := grand.S(10)
s := grand.S(10, true)
12
返回指定长度的随机字母/数字组合的字符串,第二个参数为true
表示包括特殊符号
s := grand.Digits(10)
1
返回指定长度的随机数字字符串
s := grand.Letters(10)
1
返回指定长度的随机字母字符串
s := grand.Symbols(10)
1
返回指定长度的随机特殊符号字符串
s := grand.Str("日照香炉生紫烟,遥看瀑布挂前川。Oh Yeah", 5)
1
从给定的字符串中随机返回指定数量的字符,可以是汉字。
全局唯一数
s := guid.S()
1
接口文档
用规范路由的写法,GoFrame会自动生成接口文档。所有接口信息会自动生成在/api.json
中,遵循的是OpenAPIv3
标准,框架默认使用的是redoc
来生成文档前端页面,只能查看接口信息,不能进行请求测试。因此可以可以改为其他UI页面,例如swaggerUI
或者自行实现UI页面。
注释掉manifest/config/config.yml
中的swaggerPath: "/swagger"
实现UI页面,引入swaggerUI
组件
swagger.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/swagger-ui.css">
<script src="/swagger-ui-bundle.js"></script>
<title>API Doc</title>
</head>
<body>
<div id="swagger-ui"></div>
</body>
</html>
<script type="text/javascript">
window.ui = SwaggerUIBundle({
url: '/api.json',
dom_id: '#swagger-ui'
})
</script>
123456789101112131415161718192021
引入的CSS与JS文件如果下载到项目中则用上述方式引入,或者可以通过下列CDN引入
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css" />
12
添加路由
group.GET("/swagger", func(req *ghttp.Request) {
req.Response.WriteTpl("/swagger.html")
})
123
构建打包
GoFrame中静态资源也可以直接打包进可执行文件当中,发布时只需提供一个可执行文件即可。
配置
hack/config.yaml
gfcli:
build:
name: "hellogf"
arch: "amd64"
system: "linux"
mode: "none"
cgo: 0
packSrc: "manifest/config,resource/public,resource/template"
version: "1.0.0"
output: "./bin"
extra: ""
1234567891011
name
:打包后的可执行文件名arch
:系统架构,可以有多个,用,分隔,用all
表示编译所有支持的架构system
:编译平台,可以有多个,用,分隔,用all
表示编译所有支持的系统packSrc
:需要打包的静态资源目录version
:版本号
打包
gf build
1
以上操作会把指定的目录一起打包进可执行文件。通常情况例如配置文件等一些需要改动的文件不用打包进可执行文件。
微服务部分
goframe 集成了一部分的微服务 目前主要是服务注册和发现,以及链路追踪,但是感觉在微服务这块还是不如go-zero ,如果感兴趣可以看官方文档gofram
https://goframe.org/pages/viewpage.action?pageId=1114399