快速入门goframe

go的各个开发框架比较多,个人最喜欢的是gin和gofrane,转载自https://gitee.com/unlimited13/code bilibili有教学视频
视频教学



<br



准备工作

前置条件

已安装Go语言开发环境,已配置好GOROOT、GOPATH环境变量

熟悉Go语言基本语法与使用

GoFrame文档:https://goframe.org/

学习过程以官方文档为主,本文内容均摘自官方文档,

本阶段只介绍Web开发部分,微服务部分以后有机会新开

安装框架工具

https://github.com/gogf/gf/releases

下载对应的包安装。推荐安装到GOROOT的bin目录中

用以下命令查看是否安装成功

gf -v

项目初始化

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn
# 如果已经设置过可以不要上面这两行

gf init gf_demo -u # 如果之前已经创建过项目,并且不需要创建最新版本则省略-u

常用代理地址:

  • https://goproxy.cn
  • https://goproxy.io
  • https://mirrors.aliyun.com/goproxy/

项目启动

进入项目中main.go文件所在的目录运行如下命令

gf run main.go

启动成功后,在浏览器中输入http://127.0.0.1:8000/hello查看结果



框架设计

关于框架设计的内容,有点过于抽象,内容也是偏理论的,初学就来纠结这部分基本上也难以理解,所以这部分的其他内容可以放到以后再来研究。不过也需要了解一点基础知识,比如MVC3-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{})

其中handler的定义方式有如下两种:

func(request *ghttp.Request)
func(ctx context.Context, BizRequest)(BizResponse, error)

匿名函数与普通函数注册
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
        },
    }
)

注册成功后在浏览器输入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>")
})

对于同一路由可以定义不同的请求方法实现不同功能。

几个最常用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("查询一个用户")
}

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
        },
    }
)

对象注册

对象里的方法可以批量注册

相关方法

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{})

绑定全部公共方法

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
        },
    }
)

绑定指定方法

usercontroller := user.New()
// 绑定user控制器中多个方法
s.BindObject("/user", usercontroller, "AddUser,UpdateUser")
// 绑定单个方法
s.BindObjectMethod("/deluser", usercontroller, "DeleteUser")

以RESTFul方绑定对象方法

usercontroller := user.New()
s.BindObjectRest("/user", usercontroller)

分组注册

可以为不同路由设置一个相同的前缀,即分组路由,分组路由有以下两种写法

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()
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()
		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)
			})

规范路由

GoFrame中提供了规范化的路由注册方式,注册方法如下

func Handler(ctx context.Context, req *Request) (res *Response, err error)

其中RequestResponse为自定义的结构体。

方法签名示例

方法签名需要符合 GoFrame 的要求,可以选择以下两种之一:

  • func(context.Context, *ReqStruct) (*ResStruct, error)
  • func(*ghttp.Request)

一般不适用规范路由的化就是第二种

通过如下方式指定请求方法与路径

type HelloReq struct {
    g.Meta `path:"/hello" method:"get"`
}

  1. 定义请求体和响应体

    // 请求结构体
    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"`
    }
    

    然后controller 路由层进行接收 (这里是吧路由层和逻辑层做同一处理了 正常来说路由只负责在接收前端的传递和后端的返回)

    func (c *DeviceController) ListDevices(ctx context.Context, req *device.DevicesReq) (*device.DeviceRes, error) {
        // 实际业务逻辑
        fmt.Printf("接收到的请求参数分页参数:%d\n,页码大小为:%d\n", 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
    }
    
    

    然后再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)
}

获取请求输入

普通请求输入

基础代码如下:路由用group绑定到控制器后,在控制器中写如下方法,以下代码均在此修改:

func (c *Controller) Params(request *ghttp.Request) {
    m := request.GetQueryMap()
    request.Response.WriteJson(m)
}

query参数获取

query参数是指以?a=1&b=2的形式写在url中的参数,通常由GET方法传递。


单个参数值

m := request.GetQuery("name")

GetQuery可以指定参数名称,获取对应的参数值,如果值不存在,则返回nil

还可以指定默认值,当对应参数值不存在时,返回指定的默认值

m := request.GetQuery("name", "孙行者")

返回的是一个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)

批量获取Query参数

GoFrame中提供了GetQueryMapGetQueryMapStrStrGetQueryMapStrVar三个方法用于批量获取Query参数,三个方法使用方式一致,只是返回类型不同。

  • 获取全部Query参数
m := request.GetQueryMap()
  • 指定需要获取的参数名称与默认值
m := request.GetQueryMap(map[string]interface{}{"name": "者行孙", "age": 600})

将Query参数转化为自定义结构体

可以自定义结构体,将请求参数直接转化为对应的结构体:

type user struct {
    Name string
    Age  int
}
var u *user
err := request.ParseQuery(&u)
if err != nil {
    request.Response.WritelnExit("转换出借")
}

如上,结构体中成员为NameAge,参数为nameage则参成功转换,如果结构体成员变量名与参数名不一致则无法转换,此时需要为成员变量指定其对应的参数,可以用json:/param:/p:这些方式来指定。如下

type user struct {
    UserName string `json:"name"`
    UserAge  int    `p:"age"`
}

表单参数获取(POST参数获取)

表单参数获取是指获取application/x-www-form-urlencodedapplication/form-datamultipart/form-data等数据,也可以用来获取以json格式提交的数据,简单理解即为可以获取POST方法提交的数据。


单个参数

m := request.GetForm("name")

GetForm用于指定参数名称,获取对应参数值,如果对应参数不存在,返回nil

也可以指定默认值,当指定参数不存在时,返回默认值

m := request.GetForm("name", "烧包谷")

返回的是一个gvar.Var 类型,可以根据需要进行类型转换


批量获取请求数据

可以用GetFormMapGetFormMapStrStrGetFormMapStrVar批量获取请求数据,三个方法使用方式一样,只是返回的Map类型不同。

m := request.GetFormMap()

可以指定需要获取的参数以及默认值

m := request.GetFormMap(map[string]interface{}{"name": "大洋芋"})

将请求数据转化为自定义结构体

和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("转换出借")
}

动态路由参数获取

动态路由需要对现有代码进行一点改动,需要先在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"`
}

再将控制器的的方法利用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
}

获取单个参数

u := request.GetRouter("name")

返回gvar.Var类型,可以按需要进行类型转换。也可以指定默认值。


批量获取参数

u := request.GetRouterMap()

返回值为map[string]string。如果没有设置动态路由,则返回nil


所有请求参数获取

GoFrame中还提供了一些方法获取所有请求参数,用法与上面两种类似,只是不区分请求方法。如果GET和POST提供的参数名称相同,则POST参数优先。

获取单个参数

data := request.GetRequest("name")
// 简写
data := request.Get("name")

返回gvar.Var类型,可以提供默认值。


批量获取请求参数

data := request.GetRequestMap()
// 可以指定需要获取的参数名及默认值
data := request.GetRequestMap(g.Map{"name": ""})
// 还有以下几种
data := request.GetRequestMapStrStr()
data := request.GetRequestMapStrVar()

将请求参数转为自定义结构体

request.Parse(&u) // u为自定义结构体指针

Api请求输入

api中定义请求与响应数据结构,可以直接将需要接收的参数定义为请求结构体的成员,请求时会自动转为对应结构体。

例如,将前面的api请求部分改为

type ParamReq struct {
    g.Meta   `path:"/params" method:"post"`
    UserName string `p:"name" d:"林冲"`
    UserAge  int    `p:"age" d:"110"`
}

其中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\n", req.Page, req.Limit)
	// 返回响应
	return &DeviceRes{Message: "获取设备列表成功"}, nil
}

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\n", req.Name, req.Type)
	// 返回响应
	return &DeviceRes{Message: "设备创建成功"}, nil
}
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\n", req.ID, req.Name)
	// 返回响应
	return &DeviceRes{Message: "设备更新成功"}, nil
}
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\n", req.ID)
	// 返回响应
	return &DeviceRes{Message: "获取设备详情成功"}, nil
}
总结
  • 查询参数: 适用于 GET 请求,通过 URL 传递。可以直接映射到结构体字段。
  • 请求体: 适用于 POST, PUT, PATCH 请求,传递 JSON 或其他格式的数据。使用结构体接收。
  • 表单参数: 适用于 POST 请求,表单提交的数据。使用 form 标签标记字段。
  • RESTful 路径参数: 通过路径参数传递,适用于获取特定资源或执行特定操作。使用 path 标签标记字段。

响应输出

在控制器中新建如下方法,用来测试响应输出。以下所有代码均在此处修改。

func (c *Controller) Resp(req *ghttp.Request) {
    // 以下代码在此写
}

文本数据返回

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{})

以上方法中,带有Exit的表示执行完响应之后就退出本次请求,不再执行后面的内容。带有ln的表示会在响应内容的末尾追加换行符。

以上方法用于向客户端响应文本内容。内容格式为text/htmltext/plain,参数可以是任意数据类型,非字符串类型通常会将内容进行json转为字符串后返回到客户端。

如果提供参数为文本,可以是普通文本也可以是HTML文本。


响应简单文本

req.Response.Write("锦瑟无端五十弦")

响应简单HTML

req.Response.Write("<h1>春蚕到死丝方尽</h1>")

响应复杂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" id="hr"><i></i></div>
                <div class="circle" id="mn"><i></i></div>
                <div class="circle" id="sc"><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)

格式化数据填充

html := `
    <div>姓名:%s</div>
    <div>年龄:%d</div>
    `
req.Response.Writef(html, "林黛玉", 16)

JSON数据返回

GoFrame中可以通过以下方法返回JSON数据:

func (r *Response) WriteJson(content interface{})
func (r *Response) WriteJsonExit(content interface{})

通过以上方法,会直接将参数内容进行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
}

当然如果要写响应相关逻辑的话

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
}

返回的是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

其中最常用的是WriteTpl,详细内容后面模板引擎内容里面再说,现在简单理解为该方法可以读取一个html文件,并将其返回给客户端。默认模板文件存放在resource/template下面,因此WriteTpl的第一个参数为对应的模板html文件相对于template的路径

示例:

req.Response.WriteTpl("index.html")
req.Response.WriteTpl("user/index.html")

API数据返回

现在Web应用多是前后端分离,返回数据为JSON格式,前面所说的WriteJson这样的方法只是单纯将提供的数据进行JSON转换后返回,在实际开发中,返回的JSON数据通常为以 下结构(具体项目会有差异,但基本都是类似结构):

{
    "code":0, // 自定义编码,用来表示请求成功与失败
    "msg":"请求成功", // 提示信息,如果请求出错则为错误信息
    "data":{}  // 请求返回数据,请求出错一般为null
}

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"`
}
  • 在控制器中定义对应的方法
func (c *Controller) Api(ctx context.Context, req *api.ApiReq) (res *api.ApiRes, err error) {
    return
}

实例化返回数据并返回

res = &api.ApiRes{
    UserName: "张三",
    UserAge:  120,
    List:     g.Array{1, 2, 3, 4},
}
return

如果有错误,定义错误信息并直接返回

err = gerror.Newf("服务器开小差了")
return

用上述方法返回数据,会自动返回如下格式JSON数据

{
    "code":0,
    "message":"",
    "data":{
        "name":"张三",
        "age":120,
        "list":[1,2,3,4]
    }
}

以上数据格式是通过中间件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');

数据库配置

数据库内容准备完毕后,在配置文件中进行数据库配置,只需要添加如下的内容即可

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
  • 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"

或者也可以保留上述配置,写为

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"

这样的写法使用的是link,其他的单项配置不会生效。


驱动添加与导入

  • main.go中进行MySQL驱动初始化导入
import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
)
  • go.mod中添加驱动库与版本
require (
    github.com/gogf/gf/contrib/drivers/mysql/v2 v2.5.3
    github.com/gogf/gf/v2 v2.5.3
)
  • 在命令行中进行依赖更新
go mod tidy

等等下载更新完成即可。

至此,在GoFrame中使用数据库的准备工作才准备完毕,正式进入数据库操作部分。


数据库基本操作

github.com/gogf/gf/v2/frame/g"包里的Model函数返回一个gdb.Model对象,提供了一系列对数据库的操作。Model函数接收一个参数,为数据表名:

md := g.Model("book")

返回一个与表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

返回数据库中第一条数据。查询成功返回的数据为map[string]*gvar.Var类型,所以可以直接访问里面的每一字段:

req.Response.WriteJson(bk["name"])  // 返回结果中"name"字段

可以用gvar.Var的方法对字符进行类型转换,转为需要的类型

bk["name"].String() // 转为string类型
bk["price"].Float32() // 转为float32类型

指定查询字段

bk, err := md.Fields("name, price").One() //只查询name price两个字符
// 也可以写为
bk, err := md.Fields("name", "price").One()

查询多条数据

md := g.Model("book")
bk, err := md.All()

该方法以切片返回数据表中所有数据,可以进行循环操作每一条数据

for _, v := range bk {
    req.Response.Writeln(v)
}

查询数据数量

md := g.Model("book")
count, err := md.Count()

查询一条数据指定字段

md := g.Model("book")
name, err := md.Value("name")

查询指定列数据

md := g.Model("book")
name, err := md.Array("name")

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")
Where/Where*/WhereOr/WhereOr*

使用?占位 和gorom一样 查询数据时可以通过Where方法指定条件,如果有多个Where,则多个条件之间会用AND连接

本身查询 one,all的api 也接口也接收查询语句作为参数

等于

默认情况下条件会用=连接

md := g.Model("book")
books, err := md.Where("id", 1).All()

不等

如果是不等关系,需要在字段后面加上不等符号

md := g.Model("book")
books, err := md.Where("id>", 1).All()

多个条件叠加

有多个条件时可以多个Where进行链式调用,条件会用AND连接。

md := g.Model("book")
books, err := md.Where("id>=?", 2).Where("id<?", 4).All()

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 '%数据%')

以上方法如果链式调用会生成以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 '%数据%')


Group/Order/Order*

按字段分组

md := g.Model("book")
books, err := md.Group("name").All()

按字段排序

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()

Scan

OneAll返回的数据为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)

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)

结构体数组

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)

查询结果为一个由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("结果为空")
}

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("结果为空")
}

Value

md := g.Model("book")
name, _ := md.Where("id", 10).Value("name")
if name.IsEmpty() {
    g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}

Array

md := g.Model("book")
names, _ := md.WhereLT("id", 10).Array("name")
if len(names) == 0{
    g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}

Scan结构体对象

var book *Book
md := g.Model("book")
md.Scan(&book)
if book == nil {
    g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}
md.Save(data)

Scan结构体数组

var books []Book
md := g.Model("book")
md.Scan(&books)
if len(books) == 0 {
    g.RequestFromCtx(ctx).Response.Writeln("结果为空")
}

分页

GoFrame中提供了Page方法可以很方便实现分页查询,只需提供页数和每页数据数量即可。

md := g.Model("book")
books, err := md.Page(1, 5).All()

也有Limit方法可以用来限制查询条数以及自定义起始位置与数据限制

md := g.Model("book")
// 限制条数
books, err := md.Limit(5).All()
// 指定起始位置与限制条数 (currentpage-1)*pagesize,pagesize
books, err := md.Limit(3, 5).All()

原生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
}

多表联查

xxxJOIN(“连接表”,“字段表达式”)

one, err := md.InnerJoin("user").Where("user.id = ?", id).One()
// SELECT * FROM `book` INNER JOIN `user` WHERE user.id = 1 LIMIT 1
	//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()

如果俩个字段名字一样 可以直接使用下面的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

插入数据

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)

以上方法也可配合Data使用(先绑定数据 和直接操作数据的区别)

// Insert
result, err := md.Data(data).Insert()
// Replace
result, err := md.Data(data).Replace()
// Save
result, err := md.Data(data).Save()

除了使用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()

批量写入数据

上述方法也可以批量写入数据

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()

如果使用的是结构体,将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()

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()

更新数据

Update
data := g.Map{
    "author": "郑强强",
    "price":  69.333,
}

result, err := md.Where("author", "郑强").Update(data)

也可以配合Data使用

data := g.Map{
    "author": "郑强强",
    "price":  69.333,
}

result, err := md.Where("author", "郑强").Data(data).Update()

Increment/Decrement

用来给指定字段增加/减少指定值

result, err := md.WhereBetween("id", 7, 10).Increment("price", 2.5)
result, err := md.WhereBetween("id", 7, 10).Decrement("price", 1.5)

删除数据

result, err := md.WhereGT("id", 10).Delete()

时间维护与软删除

在实际应用当中,数据表中通常会有三个时间字段:创建时间、更新时间、删除时间。GoFrame支持这三个时间字段的自动填充,这三个字段支持的类型为DATEDATETIMETIMESTAMP

  • 创建时间:默认为created_at
  • 更新时间:默认为updated_at
  • 删除时间:默认为deleted_at,数据软删除时使用

如果不想使用默认名称,需要自行修改,可以在配置文件里数据库配置时修改,方式如下:

database:
    ....
    createdAt: "create_time"
    updatedAt: "update_time"
    deletedAt: "delete_time"

软删除(逻辑删除)

软删除并不是真正从数据库中把记录删除,而是通过特定的标记在查询时过滤掉这些数据,使这些数据在页面上看不到,但实际上在数据库中仍然存在。通常用于一些需要历史追踪而不能真正删除的数据。

当数据表中有deleted_at字段时,使用Delete方法时不会物理删除数据,只是更新deleted_at字段的值。查询数据时,会自动加上WHERE `deleted_at` IS NULL这一条件,过滤掉已被“删除”的数据。

如果需要查询所有数据,需要使用Unscoped方法

ls, _ := md.Unscoped().All()

事务处理

常规写法

tx, err := g.DB().Begin(ctx)

if err == nil {
    _, err := tx.Model("book").Data(data).Save()
    if err == nil {
        tx.Commit()
    } else {
        tx.Rollback()
    }
}

闭包写法(框架建议写法)

g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
    _, err := tx.Model("book").Ctx(ctx).Save(data)
    return err
})

原生SQL的使用

Model提供的方法能组合出绝大多数使用场景所需要的数据操作,但如果需要的操作过于复杂,可能就没法通过已有的方法组合出来,就需要使用写SQL来实现

查询

db := g.DB()
books, err := db.Query(ctx, "SELECT * FROM `book` WHERE `id` > ? AND `id` < ?", g.Array{3, 7})

新增数据

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)

更多操作查看官方文档ORM方法操作(原生)



DAO自动生成与使用

数据库相关的操作与数据结构放在daomodel中,在GoFrame中,daomodel的内容可以自动生成。生成步骤如下:

  1. 配置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"
  • link: 数据库连接url
  • tables: 需要生成dao及model的数据表,多个表用逗号隔开
  • jsonCase: entity成员转为json时的转换方式,"Snake"为把驼峰转为下划线

以上为最简单配置,更多配置见官方文档代码生成/数据规范 gen dao章节

进入配置文件目录

创建internal文件夹

  1. 在命令行中执行如下命令
gf gen dao

该命令会生成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)
	}
}

该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()

字段过滤

使用结构体数据进行创建或更新数据时,尤其是在更新数据的时候,有些字段可能不需要更新,因此对应的字段就不进行赋值,例如以下代码

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()

直接这样更新,则idauthorprice也会被对应类型的零值更新,分别被更新为0、“”、0

要解决这样的问题,有以下几种解决方案:


用Fields指定需要更新的字段

dao.Book.Ctx(ctx).Fields("name", "publish_time").Where("id", 13).Data(data).Update()

用FieldsEx排除不需要更新的字段

dao.Book.Ctx(ctx).FieldsEx("id,author,price").Where("id", 13).Data(data).Update()

用OmitEmpty过滤空值

data := Book{
    Name:    "Linux驱动开发入门与实践",
    Price:   0,
    PubTime: nil,
}
dao.Book.Ctx(ctx).Where("id", 13).OmitEmpty().Data(data).Update()

用这种方法如上数据中,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()

使用这种方法,非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()

关联查询

多表数据联查时可以用连接,但是数据量大时连接效率不高,GoFrame中提供了模型关联查询,可以简化一些多表联查操作。

deptemphobby三个表为例,每个部门可以有多个员工,每个员工只有一个部门,每个员工对应一条爱好。

查询所有员工,并关联查询出其所在部门

  • 修改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"`
}
  • 使用With指定关联模型查询
var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).With(entity.Dept{}).Scan(&emps)

不用With指定关联的话,查询出的结果中Deptnil


查询所有员工,并关联查询出部门与爱好

  • 修改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"`
}
  • 使用With指定需要关联的内容
var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).With(entity.Dept{}, entity.Hobby{}).Where("dept_id", 101).Scan(&emps)

也可以用WithAll关联所有

var emps []*entity.Emp
err = dao.Emp.Ctx(ctx).WithAll().Where("dept_id", 101).Scan(&emps)

查询部门,关联查询出每个部门的员工

  • 修改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"`
}
  • 查询
var depts []*entity.Dept
err = dao.Dept.Ctx(ctx).With(entity.Emp{}).Scan(&depts)

上述关联查询直接在实体类里面修改,但实体类里的内容是用工具自动生成的,一般情况下不要修改。所以在进行关联查询时,需要重新自定义结构体,只需要保留需要查询的字段即可(用于关联的字段必须存在)

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)


自定义结构体时,需要用g.Metaorm标签指定对应的数据表



模板引擎

前面我们提过可以用下面几个方法返回模板内容

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

默认情况下模板文件目录是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"})
}

简单使用示例

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>

controller

func (c *Controller) Tpl(req *ghttp.Request) {
    data := g.Map{
        "name":   "王也道长",
        "lesson": "GoFrame入门课程",
        "num":    5,
        "what":   "模板引擎使用示例",
    }

    req.Response.WriteTpl("hello/index.html", data)
}

模板配置

模板使用当中一般情况使用默认配置即可,如果需要修改配置,则在manifest/config/config.yaml中进行修改

viewer: # 模板配置
  paths: ["resource/template", "/www/template"] # 模板路径配置,可以配置多个路径
  defaultFile: "index.hmtl" # WriteTplDefault解析的文件
  delimiters: ["${", "}"] # 模板引擎变量分隔符,默认为["{{", "}}"]

静态资源

静态资源不属于模板引擎的内容,但在模板文件中也有需要用到静态资源的地方,因此进行一下补充。

静态资源一般指的是js/css/image文件或者静态HTML文件,在GoFrame的项目目录中,这些文件放在resource/public下,之后还需要开启静态资源服务才能在模板文件中对这些资源进行引用。开启方式有两种

  • 配置文件

manifest/config/config.yaml

server:

  serverRoot:  "resource/public"
  indexFolder: true # 这个可以不用配置,放在这里了解一下
  • 用代码开启

internal/cmd/cmd.go

s := g.Server()
s.SetServerRoot("resource/public")
s.SetIndexFolder(true)

serverRoot配置了静态资源根目录resource/public,对静态资源的引用url以resource/public为根目录

例如,在resource/public/resource/css中放置了一人bootstrap.css文件,引用时写为

<link rel="stylesheet" href="/resource/css/bootstrap.css">

条件判断

在模板中可以进行条件判断,根据条件是否满足来显示不同内容,语法如下:

{{if .condition}}
条件满足时显示内容
{{else}}
条件不满足时显示内容
{{end}}

可以嵌套写,也可以写多个{{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>

controller

func (c *Controller) Tpl(req *ghttp.Request) {
    data := g.Map{
        "name":   "王也道长",
        "lesson": "GoFrame入门课程",
        "num":    5,
        "what":   "模板引擎使用示例",
    }

    req.Response.WriteTpl("hello/index.html", data)
}

大于小于等于判断

在模板中无法直接使用><==等符号进行关系判断,因此需要使用条件函数

函数对应符号
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}}

上述函数还有一些拓展用法,这里只简单介绍基础用法


逻辑判断

模板语言中可以用andornot进行逻辑运算

{{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

循环

range ... end

循环切片

controller/hello.go

data := g.Map{
    "slice": g.Array{1, 2, 3, "张楚岚", "诸葛青"},
}

req.Response.WriteTpl("hello/index.html", data)

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>

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)
}

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>


上传与下载

文件上传

文件上传可以通过表单上传,也可以通过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>

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)
        }
    }
}

多文件上传

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)
        }
    }
}

除了从请求中获取上传文件以外,如果用api规范路由,还可以用如下方式获取上传文件

type UploadReq struct {
    g.Meta `path:"/upload" method:"post"`

    Ufile ghttp.UploadFile `json:"ufile"`

    UFiles ghttp.UploadFiles `json:"ufiles"`
}

使用这种方式,如果文件允许为空, 则可能会发生转换错误。


文件上传应用实例

  • 在静态资源目录新建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
}

文件下载

ServeFile

ServeFile向客户端返回一个文件内容,如果是文本或者图片,将会直接展示,不能直接在浏览器中展示的将进行下载

func (c *Controller) Download(req *ghttp.Request) {
    req.Response.ServeFile("upload/1.png")
}

ServeFileDownload

该方法直接引导客户端进行下载,并且可以给下载文件重命名

func (c *Controller) Download(req *ghttp.Request) {
    req.Response.ServeFileDownload("upload/1.png", "download.png")
}

上传限制

如果需要限制单次上传文件大小,可以用clientMaxBodySize配置。如果完全不需要限制,直接设为0即可

config.yaml

server:
  clientMaxBodySize: "0"

数据校验

在使用中,经常需要验证前端提交过来的数据是否符合规则,比如非空、长度限制、是否为数字等一系列验证。在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
}

多个规则

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
}

使用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
}

规范路由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"`
}

controller/hello.go

func (c *Controller) Valid(ctx context.Context, rq *api.ValidReq) (rs *api.ValidRes, err error) {
    return
}

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")
}

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")
    }
}

golang-jwt

前后端分离的项目更常用的登录验证是JWT(JSON web token)。GoFrame中没有提供相关生成与验证,需要添加第三方库,例如golang-jwt

简单使用方式如下:

  • 添加
go get -u github.com/golang-jwt/jwt/v5
  • 导入
import "github.com/golang-jwt/jwt/v5"
  • 生成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)
    }
}
  • 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)
    }
}

测试

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验证失败")
	}
}


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"`
}

中间件

中间件(拦截器)是在请求与响应的过程中,拦截下请求与响应并做一些操作,常见的用途是在请求前进行鉴权,在请求后对响应数据进行包装等

中间件的定义与普通路由函数一样,只是其中需要用Next()进行路由放行。

func MiddleWare(r *ghttp.Request) {
    r.Middleware.Next()
}

前置中间件

后置中间件

在路由函数执行完成之后再进行操作的中间件称为后置中间件,例如对返回的数据格式进行统一封装的中间件:

中件间的定义实际上就是如下:

// 和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)
}

全局路由中进行绑定

s.BindMiddlewareDefault(middleware.Auth, middleware.ResponseHandler)

分组隔离的情况绑定中间件

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()
}

SetCtxVar/GetCtxVar

上下文处理 如果需要在一些请求流程中进行参数传递,可以用SetCtxVar/GetCtxVar进行存取 也是携程隔离的

例如

func MiddleWare(r *ghttp.Request) {
    r.SetCtxVar("UserName", "陆玲珑")
    r.Middleware.Next()
}

在具体路由函数中取用

UserName := r.GetCtxVar("UserName")

组件

数据结构

GoFrame中提供了一些常用的数据结构,如列表、队列、集合、Map等,详细内容见官方文档。


时间

当前时间

t := gtime.Now()
t := gtime.Date()
t := gtime.Datetime()

创建时间对象

t := gtime.New("2023-11-03 21:45:22")

参数可以是字符串、时间戳、时间对象等数据

t := gtime.NewFromStr("2023-11-03 21:50:25")
t, err := gtime.StrToTime("2023-11-03 21:50:25")

将字符串转为时间对象,具体支持的时间格式见文档时间管理/工具方法

设置时区

gtime.SetTimeZone("Asia/Tokyo")
t := gtime.Now()

时间戳

t1 := gtime.Timestamp()
t2 := gtime.TimestampMilli()
t3 := gtime.TimestampMicro()
t4 := gtime.TimestampNano()

返回为int64类型,也可以返回字符串类型

t1 := gtime.TimestampStr()
t2 := gtime.TimestampMilliStr()
t3 := gtime.TimestampMicroStr()
t4 := gtime.TimestampNanoStr()

格式化日期数据

可以将日期格式化为指定的格式,具体格式化用到的符号见文档时间管理/时间格式

t := gtime.Now()
req.Response.Writeln(t.Format("Y-m-d H:i:s"))
req.Response.Writeln(t.Format("YmdHis"))

获取年月日时分秒

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())

更多操作方法见文档时间管理/方法介绍


随机数

随机整数

n := grand.Intn(100)

返回0 <= n < 100的随机数

n := grand.N(100, 999)

返回100 <= n <= 999的随机数


随机字符串

s := grand.S(10)
s := grand.S(10, true)

返回指定长度的随机字母/数字组合的字符串,第二个参数为true表示包括特殊符号

s := grand.Digits(10)

返回指定长度的随机数字字符串

s := grand.Letters(10)

返回指定长度的随机字母字符串

s := grand.Symbols(10)

返回指定长度的随机特殊符号字符串

s := grand.Str("日照香炉生紫烟,遥看瀑布挂前川。Oh Yeah", 5)

从给定的字符串中随机返回指定数量的字符,可以是汉字。


全局唯一数

s := guid.S()


接口文档

用规范路由的写法,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>

引入的CSS与JS文件如果下载到项目中则用上述方式引入,或者可以通过下列CDN引入

<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css" />

  • 添加路由
group.GET("/swagger", func(req *ghttp.Request) {
                    req.Response.WriteTpl("/swagger.html")
                })

构建打包

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: ""
  • name:打包后的可执行文件名
  • arch:系统架构,可以有多个,用,分隔,用all表示编译所有支持的架构
  • system:编译平台,可以有多个,用,分隔,用all表示编译所有支持的系统
  • packSrc:需要打包的静态资源目录
  • version:版本号
  • 打包
gf build

以上操作会把指定的目录一起打包进可执行文件。通常情况例如配置文件等一些需要改动的文件不用打包进可执行文件。


微服务部分

goframe 集成了一部分的微服务 目前主要是服务注册和发现,以及链路追踪,但是感觉在微服务这块还是不如go-zero ,如果感兴趣可以看官方文档gofram

https://goframe.org/pages/viewpage.action?pageId=1114399

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值