GoFrame框架学习 (超长)

GF(Go Frame)是一款模块化、高性能、企业级的Go基础开发框架。完整详实的中文文档,可以减轻学习的压力。

主库:https://github.com/gogf/gf

码云:https://gitee.com/johng/gf

中文文档:https://itician.org/pages/viewpage.action?pageId=1114119

B站视频: https://www.bilibili.com/video/bv157411Z7Le

B站实战:https://www.bilibili.com/video/BV1oT4y1G7ge/

官方案例:

https://github.com/gogf/gf-demos

https://github.com/gogf/gf-home

https://github.com/CrazyRocks/goadmin  全自动一键生成的Golang admin
目录/文件 说明 描述
app 业务逻辑层 所有的业务逻辑存放目录。
–api 业务接口 接收/解析用户输入参数的入口/接口层。
–dao 数据访问 数据库的访问操作,仅包含最基础的数据库CURD方法
–model 结构模型 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义。
–service 逻辑封装 业务逻辑封装管理,实现特定的业务逻辑实现和封装。
boot 初始化包 用于项目初始化参数设置,往往作为main.go中第一个被import的包。
config 配置管理 所有的配置文件存放目录。
docker 镜像文件 Docker镜像相关依赖文件,脚本文件等等。
document 项目文档 Documentation项目文档,如: 设计文档、帮助文档等等。
i18n I18N国际化 I18N国际化配置文件目录。
library 公共库包 公共的功能封装包,往往不包含业务需求实现。
packed 打包目录 将资源文件打包的Go文件存放在这里,boot包初始化时会自动调用。
public 静态目录 仅有该目录下的文件才能对外提供静态服务访问。
router 路由注册 用于路由统一的注册管理。
template 模板文件 MVC模板文件存放的目录。
Dockerfile 镜像描述 云原生时代用于编译生成Docker镜像的描述文件。
go.mod 依赖管理 使用Go Module包管理的依赖描述文件。
main.go 入口文件 程序入口文件。

根据项目实际情况增删目录。例如,没有i18n及template需求的场景,直接删除对应目录即可。

三层架构设计与框架代码分层映射关系

新建项目

示例 https://github.com/gogf/gf-demos

项目创建推荐使用GF工具链gf init命令: https://github.com/gogf/gf-cli

安装工具: wget https://goframe.org/cli/linux_amd64/gf && chmod +x gf && ./gf install

使用中,在新建项目后,总是需要删除go.mod,然后重建。

通过gf工具建立的框架,只实现了一个hello的结构及目录搭建。

每个目录下都有一个.gitkeep文件,科普一下:作用是使Git保留一个空文件夹(我使用VSCode,可以设置为隐藏不显示,免得碍眼)

Hello World

压缩前12MB,压缩后4.6MB。

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request) {
        r.Response.Write("哈喽世界!")
    })
    s.Run()
}

静态服务器

package main

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

func main() {
	s := g.Server()
	s.SetIndexFolder(true)    //是否允许列出Server主目录的文件列表
	s.SetServerRoot("/home/ease/down")  //设置Server的主目录
	s.Run()
}

多端口监听

这还是很方便的

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("go frame!")
    })
    s.SetPort(8100, 8200, 8300)
    s.Run()
}

多实例支持

同时开启了两个端口的静态服务

package main

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

func main() {
    s1 := g.Server("s1")  //单例名称参数,该参数用于标识不同的Server实例
    s1.SetPort(8080)
    s1.SetIndexFolder(true)
    s1.SetServerRoot("/home/www/static1")
    s1.Start()

    s2 := g.Server("s2")
    s2.SetPort(8088)
    s2.SetIndexFolder(true)
    s2.SetServerRoot("/home/www/static2")
    s2.Start()

    g.Wait()
}

域名绑定

支持多域名绑定,并且不同的域名可以绑定不同的服务

package main

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

func Hello1(r *ghttp.Request) {
	r.Response.Write("127.0.0.1: Hello1!")
}

func Hello2(r *ghttp.Request) {
	r.Response.Write("localhost: Hello2!")
}

func Hello3(r *ghttp.Request) {
	r.Response.Write("外网客人你好!")
}

func main() {
	s := g.Server()
	s.Domain("127.0.0.1").BindHandler("/", Hello1)
	s.Domain("localhost").BindHandler("/", Hello2)
	s.Domain("wyyyh.dynv6.net").BindHandler("/", Hello3)
	s.SetPort(8000)
	s.Run()
}

这样也是可以的: s.Domain(“localhost1,localhost2,localhost3”).BindHandler("/", Hello2)

不支持泛域名形式

路由注册

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/{class}-{course}/:name/*act", func(r *ghttp.Request) {
        r.Response.Writef(
            "%v %v %v %v",
            r.Get("class"),
            r.Get("course"),
            r.Get("name"),
            r.Get("act"),
        )
    })
    s.SetPort(8199)
    s.Run()
}

访问测试: http://127.0.0.1:8199/class3-math/john/score

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/:name", func(r *ghttp.Request){
       r.Response.Writeln(r.Router.Uri)
    })
    s.BindHandler("/:name/update", func(r *ghttp.Request){
        r.Response.Writeln(r.Router.Uri)
    })
    s.BindHandler("/:name/:action", func(r *ghttp.Request){
        r.Response.Writeln(r.Router.Uri)
    })
    s.BindHandler("/:name/*any", func(r *ghttp.Request){
       r.Response.Writeln(r.Router.Uri)
    })
    s.BindHandler("/user/list/{field}.html", func(r *ghttp.Request){
        r.Response.Writeln(r.Router.Uri)
    })
    s.SetPort(8199)
    s.Run()
}

匹配结果:

http://127.0.0.1:8199/user/list/2.html /user/list/{field}.html http://127.0.0.1:8199/user/update /:name/update http://127.0.0.1:8199/user/info /:name/:action http://127.0.0.1:8199/user /:name/*any

这里稍有疑惑的是最后一项,为什么没有匹配/:name

由于优先级的限制,路由规则/:name会被/:name/*any规则覆盖

注册规则

package main

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

func main() {
    s := g.Server()
    // 该路由规则仅会在GET请求下有效
    s.BindHandler("GET:/{table}/list/{page}.html", func(r *ghttp.Request){
        r.Response.WriteJson(r.Router)
    })
    // 该路由规则仅会在GET请求及localhost域名下有效
    s.BindHandler("GET:/order/info/{order_id}@localhost", func(r *ghttp.Request){
        r.Response.WriteJson(r.Router)
    })
    // 该路由规则仅会在DELETE请求下有效
    s.BindHandler("DELETE:/comment/{id}", func(r *ghttp.Request){
        r.Response.WriteJson(r.Router)
    })
    s.SetPort(8199)
    s.Run()
}

r.Router是当前匹配的路由规则信息

$ curl -XGET http://127.0.0.1:8199/order/list/1.html
{"Domain":"default","Method":"GET","Priority":3,"Uri":"/{table}/list/{page}.html"}

$ curl -XGET http://127.0.0.1:8199/order/info/1
Not Found

$ curl -XGET http://localhost:8199/order/info/1
{"Domain":"localhost","Method":"GET","Priority":3,"Uri":"/order/info/{order_id}"}

$ curl -XDELETE http://127.0.0.1:8199/comment/1000
{"Domain":"default","Method":"DELETE","Priority":2,"Uri":"/comment/{id}"}

$ curl -XGET http://127.0.0.1:8199/comment/1000
Not Found

动态路由规则

  1. 精准匹配规则 即指定匹配字符

  2. 命名匹配规则 例如:name,必须有值

  3. 模糊匹配规则 使用*any方式进行匹配,匹配内容可以为空

  4. 字段匹配规则 参数进行截取匹配,即匹配区间会有一部份精准匹配,而另一部份则为参数

rule: /db-{table}/{id}

/db-user/1                     match
/db-user/2                     match
/db/user/1                     no match
/db-order/100                  match
/database-order/100            no match

优先级控制

  1. 层级越深的规则优先级越高;
  2. 同一层级下,精准匹配优先级高于模糊匹配;
  3. 同一层级下,模糊匹配优先级:字段匹配 > 命名匹配 > 模糊匹配;

路由管理

使用BindHandler方法进行路由注册的方式叫做回调函数注册,是最简单的一种路由注册方式。

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request) {
        r.Response.Write("哈喽世界!")
    })
    s.Run()
}

以下示例将回调绑定在指定域名下,其它域名不可访问

package main

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

func main() {
    d := g.Server().Domain("localhost")
    d.BindHandler("/", func(r *ghttp.Request) {
        r.Response.Write("Hello World!")
    })
    g.Server().Run()
}
包方法注册
package main

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

var (
	total = gtype.NewInt()
)

func Total(r *ghttp.Request) {
	r.Response.Write("total:", total.Add(1))
}

func main() {
	s := g.Server()
	s.BindHandler("/total", Total)
	s.SetPort(8199)
	s.Run()
}
对象方法注册
package main

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

type Controller struct {
	total *gtype.Int
}

func (c *Controller) Total(r *ghttp.Request) {
	r.Response.Write("total:", c.total.Add(1))
}

func main() {
	s := g.Server()
	c := &Controller{
		total: gtype.NewInt(),
	}
	s.BindHandler("/total", c.Total)
	s.SetPort(8199)
	s.Run()
}

对象注册

package main

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

type Controller struct{}

func (c *Controller) Index(r *ghttp.Request) {
	r.Response.Write("index")
}

func (c *Controller) Show(r *ghttp.Request) {
	r.Response.Write("show")
}

func main() {
	s := g.Server()
	c := new(Controller)
	s.BindObject("/object", c)
	s.SetPort(8199)
	s.Run()
}

生成如下路由表

  SERVER  | DOMAIN  | ADDRESS | METHOD |     ROUTE     |         HANDLER          | MIDDLEWARE
|---------|---------|---------|--------|---------------|--------------------------|------------|
  default | default | :8199   | ALL    | /object       | main.(*Controller).Index |
|---------|---------|---------|--------|---------------|--------------------------|------------|
  default | default | :8199   | ALL    | /object/index | main.(*Controller).Index |
|---------|---------|---------|--------|---------------|--------------------------|------------|
  default | default | :8199   | ALL    | /object/show  | main.(*Controller).Show  |
|---------|---------|---------|--------|---------------|--------------------------|------------|

即,所有/object下的路由,均由c进行处理

路由内置变量

在路由规则中可以使用两个内置的变量:{.struct}和{.method},前者表示当前对象名称,后者表示当前注册的方法名。

package main

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

type Order struct{}

func (o *Order) List(r *ghttp.Request) {
	r.Response.Write("list")
}

func main() {
    s := g.Server()
    o := new(Order)
	s.BindObject("/{.struct}-{.method}", o)
	s.SetPort(8199)
	s.Run()
}

产生如下路由表

  SERVER  | DOMAIN  | ADDRESS | METHOD |    ROUTE    |      HANDLER       | MIDDLEWARE
|---------|---------|---------|--------|-------------|--------------------|------------|
  default | default | :8199   | ALL    | /order-list | main.(*Order).List |
|---------|---------|---------|--------|-------------|--------------------|------------|

命名风格规则

当方法名称带有多个单词(按照字符大写区分单词)时,路由控制器默认会自动使用英文连接符号-进行拼接,因此访问的时候方法名称需要带-号。

以下示例使用了几种内置的转换方法

package main

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

type User struct{}

func (u *User) ShowList(r *ghttp.Request) {
	r.Response.Write("list")
}

func main() {
	u := new(User)
	s1 := g.Server("DEFAULT ") 
	s2 := g.Server("FULLNAME")
	s3 := g.Server("ALLLOWER")
	s4 := g.Server("CAMEL   ")

	s1.SetNameToUriType(ghttp.URI_TYPE_DEFAULT)  // (默认)全部转为小写,单词以'-'连接符号连接
	s2.SetNameToUriType(ghttp.URI_TYPE_FULLNAME) // 不处理名称,以原有名称构建成URI
	s3.SetNameToUriType(ghttp.URI_TYPE_ALLLOWER) // 仅转为小写,单词间不使用连接符号
	s4.SetNameToUriType(ghttp.URI_TYPE_CAMEL)    // 采用驼峰命名方式

	s1.BindObject("/{.struct}/{.method}", u)
	s2.BindObject("/{.struct}/{.method}", u)
	s3.BindObject("/{.struct}/{.method}", u)
	s4.BindObject("/{.struct}/{.method}", u)

	s1.SetPort(8100)
	s2.SetPort(8200)
	s3.SetPort(8300)
	s4.SetPort(8400)

	s1.Start()
	s2.Start()
	s3.Start()
	s4.Start()

	g.Wait()
}

产生路由

  SERVER  | DOMAIN  | ADDRESS | METHOD |      ROUTE      |        HANDLER        | MIDDLEWARE
|----------|---------|---------|--------|-----------------|-----------------------|------------|
  DEFAULT  | default | :8100   | ALL    | /user/show-list | main.(*User).ShowList |
|----------|---------|---------|--------|-----------------|-----------------------|------------|

   SERVER  | DOMAIN  | ADDRESS | METHOD |     ROUTE       |        HANDLER        | MIDDLEWARE
|----------|---------|---------|--------|-----------------|-----------------------|------------|
  FULLNAME | default | :8200   | ALL    | /User/ShowList  | main.(*User).ShowList |
|----------|---------|---------|--------|-----------------|-----------------------|------------|

   SERVER  | DOMAIN  | ADDRESS | METHOD |     ROUTE       |        HANDLER        | MIDDLEWARE
|----------|---------|---------|--------|-----------------|-----------------------|------------|
  ALLLOWER | default | :8300   | ALL    | /user/showlist  | main.(*User).ShowList |
|----------|---------|---------|--------|-----------------|-----------------------|------------|

   SERVER  | DOMAIN  | ADDRESS | METHOD |     ROUTE       |        HANDLER        | MIDDLEWARE
|----------|---------|---------|--------|-----------------|-----------------------|------------|
  CAMEL    | default | :8400   | ALL    | /user/showList  | main.(*User).ShowList |
|----------|---------|---------|--------|-----------------|-----------------------|------------|

对象方法注册

只注册其中几个。以下示例中,只注册/公开了Show方法,多个方法用逗号隔开。

package main

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

type Controller struct{}

func (c *Controller) Index(r *ghttp.Request) {
	r.Response.Write("index")
}

func (c *Controller) Show(r *ghttp.Request) {
	r.Response.Write("show")
}

func main() {
	s := g.Server()
	c := new(Controller)
	s.BindObject("/object", c, "Show")
	s.SetPort(8199)
	s.Run()
}

绑定路由方法

通过BindObjectMethod方法绑定指定的路由到指定的方法执行

package main

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

type Controller struct{}

func (c *Controller) Index(r *ghttp.Request) {
	r.Response.Write("index")
}

func (c *Controller) Show(r *ghttp.Request) {
	r.Response.Write("show")
}

func main() {
	s := g.Server()
	c := new(Controller)
	s.BindObjectMethod("/show", c, "Show")
	s.SetPort(8199)
	s.Run()
}

与之前的s.BindObject("/object", c, “Show”)有些差别。例如,产生的路由表不同,这里产生的是:

  SERVER  | DOMAIN  | ADDRESS | METHOD | ROUTE |         HANDLER         | MIDDLEWARE
|---------|---------|---------|--------|-------|-------------------------|------------|
  default | default | :8199   | ALL    | /show | main.(*Controller).Show |
|---------|---------|---------|--------|-------|-------------------------|------------|

RESTful对象注册

通常用于API服务

package main

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

type Controller struct{}

// RESTFul - GET
func (c *Controller) Get(r *ghttp.Request) {
	r.Response.Write("GET")
}

// RESTFul - POST
func (c *Controller) Post(r *ghttp.Request) {
	r.Response.Write("POST")
}

// RESTFul - DELETE
func (c *Controller) Delete(r *ghttp.Request) {
	r.Response.Write("DELETE")
}

// 该方法无法映射,将会无法访问到
func (c *Controller) Hello(r *ghttp.Request) {
	r.Response.Write("Hello")
}

func main() {
	s := g.Server()
	c := new(Controller)
	s.BindObjectRest("/object", c)
	s.SetPort(8199)
	s.Run()
}
  SERVER  | DOMAIN  | ADDRESS | METHOD |  ROUTE  |          HANDLER          | MIDDLEWARE
|---------|---------|---------|--------|---------|---------------------------|------------|
  default | default | :8199   | DELETE | /object | main.(*Controller).Delete |
|---------|---------|---------|--------|---------|---------------------------|------------|
  default | default | :8199   | GET    | /object | main.(*Controller).Get    |
|---------|---------|---------|--------|---------|---------------------------|------------|
  default | default | :8199   | POST   | /object | main.(*Controller).Post   |
|---------|---------|---------|--------|---------|---------------------------|------------|

构造方法Init与析构方法Shut

// “构造函数"对象方法 // 对象收到请求时的初始化方法,在服务接口调用之前被回调执行。 func (c *Controller) Init(r *ghttp.Request) {}

// “析构函数"对象方法 // 当请求结束时被Server自动调用,可以用于对象执行一些收尾处理的操作。 func (c *Controller) Shut(r *ghttp.Request) {}

package main

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

type Controller struct{}

func (c *Controller) Init(r *ghttp.Request) {
	r.Response.Writeln("Init")
}

func (c *Controller) Shut(r *ghttp.Request) {
	r.Response.Writeln("Shut")
}

func (c *Controller) Hello(r *ghttp.Request) {
	r.Response.Writeln("Hello")
}

func main() {
	s := g.Server()
	c := new(Controller)
	s.BindObject("/object", c)
	s.SetPort(8199)
	s.Run()
}

访问 http://127.0.0.1:8199/object/hello 后,输出结果为:Init、Hello、Shut,而路由表中并不会有这两个方法

分组路由

package main

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

func main() {
	s := g.Server()
	group := s.Group("/api")
	group.ALL("/all", func(r *ghttp.Request) {
		r.Response.Write("all")
	})
	group.GET("/get", func(r *ghttp.Request) {
		r.Response.Write("get")
	})
	group.POST("/post", func(r *ghttp.Request) {
		r.Response.Write("post")
	})
	s.SetPort(8199)
	s.Run()
}

生成路由表

  SERVER  | DOMAIN  | ADDRESS | METHOD |   ROUTE   |     HANDLER     | MIDDLEWARE
|---------|---------|---------|--------|-----------|-----------------|------------|
  default | default | :8199   | ALL    | /api/all  | main.main.func1 |
|---------|---------|---------|--------|-----------|-----------------|------------|
  default | default | :8199   | GET    | /api/get  | main.main.func2 |
|---------|---------|---------|--------|-----------|-----------------|------------|
  default | default | :8199   | POST   | /api/post | main.main.func3 |
|---------|---------|---------|--------|-----------|-----------------|------------|

层级注册

批量注册

s := g.Server()

// 前台系统自定义错误页面
s.BindStatusHandler(401, func(r *ghttp.Request) {
	if !gstr.HasPrefix(r.URL.Path, "/admin") {
		service.View.Render401(r)
	}
})
s.BindStatusHandler(403, func(r *ghttp.Request) {
	if !gstr.HasPrefix(r.URL.Path, "/admin") {
		service.View.Render403(r)
	}
})
s.BindStatusHandler(404, func(r *ghttp.Request) {
	if !gstr.HasPrefix(r.URL.Path, "/admin") {
		service.View.Render404(r)
	}
})
s.BindStatusHandler(500, func(r *ghttp.Request) {
	if !gstr.HasPrefix(r.URL.Path, "/admin") {
		service.View.Render500(r)
	}
})

// 前台系统路由注册
s.Group("/", func(group *ghttp.RouterGroup) {
	group.Middleware(service.Middleware.Ctx)
	group.ALLMap(g.Map{
		"/":            api.Index,          // 首页
		"/login":       api.Login,          // 登录
		"/register":    api.Register,       // 注册
		"/category":    api.Category,       // 栏目
		"/topic":       api.Topic,          // 主题
		"/topic/:id":   api.Topic.Detail,   // 主题 - 详情
		"/ask":         api.Ask,            // 问答
		"/ask/:id":     api.Ask.Detail,     // 问答 - 详情
		"/article":     api.Article,        // 文章
		"/article/:id": api.Article.Detail, // 文章 - 详情
		"/reply":       api.Reply,          // 回复
		"/search":      api.Search,         // 搜索
		"/captcha":     api.Captcha,        // 验证码
		"/user/:id":    api.User.Index,     // 用户 - 主页
	})
	// 权限控制路由
	group.Group("/", func(group *ghttp.RouterGroup) {
		group.Middleware(service.Middleware.Auth)
		group.ALLMap(g.Map{
			"/user":     api.User,     // 用户
			"/content":  api.Content,  // 内容
			"/interact": api.Interact, // 交互
			"/file":     api.File,     // 文件
		})
	})
})

中间件/拦截器

中间件定义
func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()  //进一步执行下一个流程
}
中间件类型

前置中间件和后置中间件。前置即在路由服务函数调用之前调用,后置即在其后调用。(先处理自己还是先处理别人)

func Middleware(r *ghttp.Request) {
	// 中间件处理逻辑
	r.Middleware.Next()
}

func Middleware(r *ghttp.Request) {
	r.Middleware.Next()
	// 中间件处理逻辑
}
中间件注册

全局中间件

BindMiddleware方法是将中间件注册到指定的路由规则下,中间件参数可以给定多个。

BindMiddlewareDefault方法是将中间件注册到/*全局路由规则下。

Use方法是BindMiddlewareDefault别名。

全局中间件仅对动态请求拦截有效,无法拦截静态文件请求

分组路由中间件

绑定到当前分组路由中的所有的服务请求上

示例1,允许跨域请求
package main

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

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS)
		group.ALL("/user/list", func(r *ghttp.Request) {
			r.Response.Writeln("list")
		})
	})
	s.SetPort(8199)
	s.Run()
}
示例2,请求鉴权处理
package main

import (
	"net/http"

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

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token == "123456" {
		r.Response.Writeln("auth")
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.Writeln("cors")
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS, MiddlewareAuth)
		group.ALL("/user/list", func(r *ghttp.Request) {
			r.Response.Writeln("list")
		})
	})
	s.SetPort(8199)
	s.Run()
}

http://127.0.0.1:8199/api.v2/user/list 和 http://127.0.0.1:8199/api.v2/user/list?token=123456 对比来查看效果

示例3,鉴权例外处理

以下示例中,/admin下/login不作权限检测(尚未登陆),其余均需

package main

import (
	"net/http"

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

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func main() {
	s := g.Server()
	s.Group("/admin", func(group *ghttp.RouterGroup) {
		group.ALL("/login", func(r *ghttp.Request) {
			r.Response.Writeln("login")
		})
		group.Group("/", func(group *ghttp.RouterGroup) {
			group.Middleware(MiddlewareAuth)
			group.ALL("/dashboard", func(r *ghttp.Request) {
				r.Response.Writeln("dashboard")
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}
示例4,统一的错误处理
package main

import (
	"net/http"

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

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if r.Response.Status >= http.StatusInternalServerError {
		r.Response.ClearBuffer()
		r.Response.Writeln("哎哟我去,服务器居然开小差了,请稍后再试吧!")
	}
}

func main() {
	s := g.Server()
	s.Use(MiddlewareCORS)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareAuth, MiddlewareErrorHandler)
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("db error: sql is xxxxxxx")
		})
	})
	s.SetPort(8199)
	s.Run()
}
示例5,自定义日志处理
package main

import (
	"net/http"

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

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func MiddlewareLog(r *ghttp.Request) {
	r.Middleware.Next()
	errStr := ""
	if err := r.GetError(); err != nil {
		errStr = err.Error()
	}
	g.Log().Println(r.Response.Status, r.URL.Path, errStr)
}

func main() {
	s := g.Server()
	s.SetConfigWithMap(g.Map{
		"AccessLogEnabled": false,
		"ErrorLogEnabled":  false,
	})
	s.Use(MiddlewareLog, MiddlewareCORS)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareAuth)
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("啊!我出错了!")
		})
	})
	s.SetPort(8199)
	s.Run()
}

请求输入

Get*: 常用方法,简化参数获取,GetRequest*的别名。
GetQuery*: 获取GET方式传递过来的参数,包括Query String及Body参数解析。
GetForm*: 获取表单方式传递过来的参数,表单方式提交的参数Content-Type往往为application/x-www-form-urlencoded, application/form-data, multipart/form-data, multipart/mixed等等。
GetRequest*: 获取客户端提交的参数,不区分提交方式。
Get*Struct: 将指定类型的请求参数绑定到指定的struct对象上,注意给定的参数为对象指针。绝大部分场景中往往使用Parse方法将请求数据转换为请求对象,具体详见后续章节。
GetBody/GetBodyString: 获取客户端提交的原始数据,该数据是客户端写入到body中的原始数据,与HTTP Method无关,例如客户端提交JSON/XML数据格式时可以通过该方法获取原始的提交数据。
GetJson: 自动将原始请求信息解析为gjson.Json对象指针返回,gjson.Json对象具体在 gjson (数据动态编解码) 章节中介绍。
Exit*: 用于请求流程退出控制,详见本章后续说明;

参数类型

GetQueryString、GetQueryFloat32、GetQueryFloat64、float32、float64、GetQueryInt、GetQueryUint

转换,例如:GetVar(“id”).Int8()

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Writeln(r.Get("amount"))
		r.Response.Writeln(r.GetInt("amount"))
		r.Response.Writeln(r.GetFloat32("amount"))
	})
	s.SetPort(8199)
	s.Run()
}

复杂参数

参数 变量
k1=m&k2=n map[k1:m k2:n]
k=m&k=n map[k:n]
k=m&k[a]=n error
k[]=m&k[]=n map[k:[m n]]
k[a]=m&k[b]=n map[k:map[a:m b:n]]
k[a][]=m&k[a][]=n map[k:map[a:[m n]]]
k[a][a]=m&k[a][b]=n map[k:map[a:map[a:m b:n]]]

同名参数

同名参数提交格式形如:k=v1&k=v2 ,后续的变量值将会覆盖前面的变量值。

数组参数

数组参数提交格式形如:k[]=v1&k[]=v2

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Write(r.Get("array"))
	})
	s.SetPort(8199)
	s.Run()
}

访问 http://127.0.0.1:8199/?array[]=john&array[]=smith 后,将会得到返回值 [“john”,“smith”]

Map参数

提交格式形如:k[a]=m&k[b]=n,并且支持多级Map,例如:k[a][a]=m&k[a][b]=n

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Write(r.Get("map"))
	})
	s.SetPort(8199)
	s.Run()
}

访问 http://127.0.0.1:8199/?map[id]=1&map[name]=john 后,将会得到返回值 {“id”:“1”,“name”:“john”}

参数映射

struct中需要匹配的属性必须为公开属性(首字母大写)。
参数名称会自动按照 不区分大小写 且 忽略-/_/空格符号 的形式与struct属性进行匹配。
如果匹配成功,那么将键值赋值给属性,如果无法匹配,那么忽略该键值。

示例:

map键名    struct属性     是否匹配
name       Name           match
Email      Email          match
nickname   NickName       match
NICKNAME   NickName       match
Nick-Name  NickName       match
nick_name  NickName       match
nick name  NickName       match
NickName   Nick_Name      match
Nick-name  Nick_Name      match
nick_name  Nick_Name      match
nick name  Nick_Name      match

自定义规则

type User struct{
    Id    int
    Name  string
    Pass1 string `p:"password1"`
    Pass2 string `p:"password2"`
}

使用了p标签来指定该属性绑定的参数名称。password1参数将会映射到Pass1属性,password2将会映射到Pass2属性上

Parse转换

package main

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

type RegisterReq struct {
	Name  string
	Pass  string `p:"password1"`
	Pass2 string `p:"password2"`
}

type RegisterRes struct {
	Code  int         `json:"code"`
	Error string      `json:"error"`
	Data  interface{} `json:"data"`
}

func main() {
	s := g.Server()
	s.BindHandler("/register", func(r *ghttp.Request) {
		var req *RegisterReq
		if err := r.Parse(&req); err != nil {
			r.Response.WriteJsonExit(RegisterRes{
				Code:  1,
				Error: err.Error(),
			})
		}
		// ...
		r.Response.WriteJsonExit(RegisterRes{
			Data: req,
		})
	})
	s.SetPort(8199)
	s.Run()
}

测试:

$ curl "http://127.0.0.1:8199/register?name=john&password1=123&password2=456"
{"code":0,"error":"","data":{"Name":"john","Pass":"123","Pass2":"456"}}

$ curl -d "name=john&password1=123&password2=456" -X POST "http://127.0.0.1:8199/register"
{"code":0,"error":"","data":{"Name":"john","Pass":"123","Pass2":"456"}}

请求校验

示例1,基本使用

属性绑定v标签

package main

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

// 注册请求数据结构
type RegisterReq struct {
	Name  string `p:"username"  v:"required|length:6,30#请输入账号|账号长度为:min到:max位"`
	Pass  string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"`
	Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"`
}

// 注册返回数据结构
type RegisterRes struct {
	Code  int         `json:"code"`
	Error string      `json:"error"`
	Data  interface{} `json:"data"`
}

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/register", func(r *ghttp.Request) {
			var req *RegisterReq
			if err := r.Parse(&req); err != nil {
				r.Response.WriteJsonExit(RegisterRes{
					Code:  1,
					Error: err.Error(),
				})
			}
			// ...
			r.Response.WriteJsonExit(RegisterRes{
				Data: req,
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

测试: curl “http://127.0.0.1:8199/register?name=john&password1=123456&password2=123456”

示例2,校验错误处理


package main

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

type RegisterReq struct {
	Name  string `p:"username"  v:"required|length:6,30#请输入账号|账号长度为:min到:max位"`
	Pass  string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"`
	Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"`
}

type RegisterRes struct {
	Code  int         `json:"code"`
	Error string      `json:"error"`
	Data  interface{} `json:"data"`
}

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/register", func(r *ghttp.Request) {
			var req *RegisterReq
			if err := r.Parse(&req); err != nil {
				// Validation error.
				if v, ok := err.(*gvalid.Error); ok {
					r.Response.WriteJsonExit(RegisterRes{
						Code:  1,
						Error: v.FirstString(),
					})
				}
				// Other error.
				r.Response.WriteJsonExit(RegisterRes{
					Code:  1,
					Error: err.Error(),
				})
			}
			// ...
			r.Response.WriteJsonExit(RegisterRes{
				Data: req,
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

通过err.(*gvalid.Error)断言的方式判断错误是否为校验错误

也可以使用gerror.Current获取第一条报错信息

JSON/XML

示例1,简单示例
package main

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

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Writef("name: %v, pass: %v", r.Get("name"), r.Get("pass"))
	})
	s.SetPort(8199)
	s.Run()
}

测试不同的提交方法:

Query数据格式 curl “http://127.0.0.1:8199/?name=john&pass=123”

Form表单提交 curl -d “name=john&pass=123” “http://127.0.0.1:8199/”

JSON数据格式 $ curl -d ‘{“name”:“john”,“pass”:“123”}’ “http://127.0.0.1:8199/”

XML数据格式 curl -d ‘john123’ “http://127.0.0.1:8199/”

XML数据格式 curl -d ‘john123’ “http://127.0.0.1:8199/”

示例2,对象转换及校验
package main

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

type RegisterReq struct {
	Name  string `p:"username"  v:"required|length:6,30#请输入账号|账号长度为:min到:max位"`
	Pass  string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"`
	Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"`
}

type RegisterRes struct {
	Code  int         `json:"code"`
	Error string      `json:"error"`
	Data  interface{} `json:"data"`
}

func main() {
	s := g.Server()
	s.BindHandler("/register", func(r *ghttp.Request) {
		var req *RegisterReq
		if err := r.Parse(&req); err != nil {
			// Validation error.
			if v, ok := err.(*gvalid.Error); ok {
				r.Response.WriteJsonExit(RegisterRes{
					Code:  1,
					Error: v.FirstString(),
				})
			}
			// Other error.
			r.Response.WriteJsonExit(RegisterRes{
				Code:  1,
				Error: err.Error(),
			})
		}
		// ...
		r.Response.WriteJsonExit(RegisterRes{
			Data: req,
		})
	})
	s.SetPort(8199)
	s.Run()
}

默认值绑定

默认值的struct tag名称为d

type ContentServiceGetListReq struct {
	Type       string                                    // 内容模型
	CategoryId uint   `p:"cate"`                         // 栏目ID
	Page       int    `d:"1"  v:"min:0#分页号码错误"`      // 分页号码
	Size       int    `d:"10" v:"max:50#分页数量最大50条"` // 分页数量,最大50
	Sort       int    // 排序类型(0:最新, 默认。1:活跃, 2:热度)
}

自定义变量

自定义变量的获取优先级是最高的,可以覆盖原有的客户端提交参数。

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

// 前置中间件1
func MiddlewareBefore1(r *ghttp.Request) {
	r.SetParam("name", "GoFrame")
	r.Response.Writeln("set name")
	r.Middleware.Next()
}

// 前置中间件2
func MiddlewareBefore2(r *ghttp.Request) {
	r.SetParam("site", "https://goframe.org")
	r.Response.Writeln("set site")
	r.Middleware.Next()
}

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareBefore1, MiddlewareBefore2)
		group.ALL("/", func(r *ghttp.Request) {
			r.Response.Writefln(
				"%s: %s",
				r.GetParamVar("name").String(),
				r.GetParamVar("site").String(),
			)
		})
	})
	s.SetPort(8199)
	s.Run()
}

Context

推荐使用Context上下文对象来处理流程共享的上下文变量,甚至将该对象进一步传递到依赖的各个模块方法中

GetCtx方法用于获取当前的context.Context对象,作用同Context方法。
SetCtx方法用于设置自定义的context.Context上下文对象。
GetCtxVar方法用于获取上下文变量,并可给定当该变量不存在时的默认值。
SetCtxVar方法用于设置上下文变量
示例1,SetCtxVar/GetCtxVar
package main

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

const (
	TraceIdName = "trace-id"
)

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.Middleware(func(r *ghttp.Request) {
			r.SetCtxVar(TraceIdName, "HBm876TFCde435Tgf")
			r.Middleware.Next()
		})
		group.ALL("/", func(r *ghttp.Request) {
			r.Response.Write(r.GetCtxVar(TraceIdName))
		})
	})
	s.SetPort(8199)
	s.Run()
}
示例2,SetCtx

SetCtx方法常用于中间件中整合一些第三方的组件,例如第三方的链路跟踪组件等等。



package main

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

const (
	TraceIdName = "trace-id"
)

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.Middleware(func(r *ghttp.Request) {
			ctx := context.WithValue(r.Context(), TraceIdName, "HBm876TFCde435Tgf")
			r.SetCtx(ctx)
			r.Middleware.Next()
		})
		group.ALL("/", func(r *ghttp.Request) {
			r.Response.Write(r.Context().Value(TraceIdName))
			// 也可以使用
			// r.Response.Write(r.GetCtxVar(TraceIdName))
		})
	})
	s.SetPort(8199)
	s.Run()
}

Exit控制

  1. Exit: 仅退出当前执行的逻辑方法,不退出后续的请求流程,可用于替代return。
  2. ExitAll: 强行中断当前执行流程,当前执行方法的后续逻辑以及后续所有的逻辑方法将不再执行,常用于权限控制。
  3. ExitHook: 当路由匹配到多个HOOK方法时,默认是按照路由匹配优先级顺序执行HOOK方法。当在HOOK方法中调用ExitHook方法后,后续的HOOK方法将不会被继续执行,作用类似HOOK方法覆盖。
  4. 这三个退出函数仅在服务函数和HOOK事件回调函数中有效,无法控制中间件的执行流程。
Exit返回方法

原始代码

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		if r.GetInt("type") == 1 {
			r.Response.Writeln("john")
		}
		r.Response.Writeln("smith")
	})
	s.SetPort(8199)
	s.Run()
}

使用Exit的代码

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		if r.GetInt("type") == 1 {
            r.Response.Writeln("john")
            r.Exit()
		}
		r.Response.Writeln("smith")
	})
	s.SetPort(8199)
	s.Run()
}

文件上传

上传文件大小受到ghttp.Server的ClientMaxBodySize 配置影响,默认支持的上传文件大小为8MB

服务端

https://github.com/gogf/gf/blob/master/.example/net/ghttp/client/upload/server.go

package main

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

// Upload uploads files to /tmp .
func Upload(r *ghttp.Request) {
	files := r.GetUploadFiles("upload-file")
    names, err := files.Save("/tmp/")
    if err != nil {
		r.Response.WriteExit(err)
	}
	r.Response.WriteExit("upload successfully: ", names)
}

// UploadShow shows uploading simgle file page.
func UploadShow(r *ghttp.Request) {
	r.Response.Write(`
    <html>
    <head>
        <title>GF Upload File Demo</title>
    </head>
        <body>
            <form enctype="multipart/form-data" action="/upload" method="post">
                <input type="file" name="upload-file" />
                <input type="submit" value="upload" />
            </form>
        </body>
    </html>
    `)
}

// UploadShowBatch shows uploading multiple files page.
func UploadShowBatch(r *ghttp.Request) {
	r.Response.Write(`
    <html>
    <head>
        <title>GF Upload Files Demo</title>
    </head>
        <body>
            <form enctype="multipart/form-data" action="/upload" method="post">
                <input type="file" name="upload-file" />
                <input type="file" name="upload-file" />
                <input type="submit" value="upload" />
            </form>
        </body>
    </html>
    `)
}

func main() {
	s := g.Server()
	s.Group("/upload", func(group *ghttp.RouterGroup) {
		group.POST("/", Upload)
		group.ALL("/show", UploadShow)
		group.ALL("/batch", UploadShowBatch)
	})
	s.SetPort(8199)
	s.Run()
}
  1. r.GetUploadFiles方法获得上传的所有文件对象,也可以通过r.GetUploadFile获取单个上传的文件对象。
  2. 在r.GetUploadFiles(“upload-file”)中的参数"upload-file"为本示例中客户端上传时的表单文件域名称,开发者可以根据前后端约定在客户端中定义,以方便服务端接收表单文件域参数。
  3. 通过files.Save可以将上传的多个文件方便地保存到指定的目录下,并返回保存成功的文件名。如果是批量保存,只要任意一个文件保存失败,都将会立即返回错误。此外,Save方法的第二个参数支持随机自动命名上传文件。
  4. 通过group.POST(”/”, Upload)注册的路由仅支持POST方式访问

客户端

单文件上传

https://github.com/gogf/gf/blob/master/.example/net/ghttp/client/upload/client.go

package main

import (
    "fmt"

    "github.com/gogf/gf/net/ghttp"
    "github.com/gogf/gf/os/glog"
)

func main() {
    path := "/home/john/Workspace/Go/github.com/gogf/gf/version.go"
    r, e := ghttp.Post("http://127.0.0.1:8199/upload", "upload-file=@file:"+path)
    if e != nil {
        glog.Error(e)
    } else {
        fmt.Println(string(r.ReadAll()))
        r.Close()
    }
}

多文件上传

https://github.com/gogf/gf/blob/master/.example/net/ghttp/client/upload-batch/client.go

package main

import (
	"fmt"
	"github.com/gogf/gf/net/ghttp"
	"github.com/gogf/gf/os/glog"
)

func main() {
	path1 := "/Users/john/Pictures/logo1.png"
	path2 := "/Users/john/Pictures/logo2.png"
	r, e := ghttp.Post(
		"http://127.0.0.1:8199/upload",
		fmt.Sprintf(`upload-file=@file:%s&upload-file=@file:%s`, path1, path2),
	)
	if e != nil {
		glog.Error(e)
	} else {
		fmt.Println(string(r.ReadAll()))
		r.Close()
	}
}

自定义文件名称

s := g.Server()
s.BindHandler("/upload", func(r *ghttp.Request) {
    file := r.GetUploadFile("TestFile")
    if file == nil {
        r.Response.Write("empty file")
        return
    }
    file.Filename = "MyCustomFileName.txt"
    fileName, err := file.Save(gfile.TempDir())
    if err != nil {
        r.Response.Write(err)
        return
    }
    r.Response.Write(fileName)
})
s.SetPort(8999)
s.Run()

数据返回

  1. Write*方法用于数据的输出,可为任意的数据格式,内部通过断言对参数做自动分析。
  2. Write*Exit方法用于数据输出后退出当前服务方法,可用于替代return返回方法。
  3. WriteJson*/WriteXml方法用于特定数据格式的输出,这是为开发者提供的简便方法。
  4. WriteTpl*方法用于模板输出,解析并输出模板文件,也可以直接解析并输出给定的模板内容。
  5. ParseTpl*方法用于模板解析,解析模板文件或者模板内容,返回解析后的内容。
  6. 其他方法详见接口文档;

此外,需要提一下,Header的操作可以通过标准库的方法来实现,例如:

Response.Header().Set(“Content-Type”, “text/plain; charset=utf-8”)

缓冲控制

func (r *Response) Buffer() []byte
func (r *Response) BufferString() string
func (r *Response) BufferLength() int
func (r *Response) SetBuffer(data []byte)
func (r *Response) ClearBuffer()
func (r *Response) Output()

示例:通过后置中间件统一对返回的数据做处理,如果服务方法产生了异常,不将敏感错误信息推送到客户端,而统一设置错误提示信息。

package main

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

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if r.Response.Status >= http.StatusInternalServerError {
		r.Response.ClearBuffer()
		r.Response.Writeln("服务器居然开小差了,请稍后再试吧!")
	}
}

func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareErrorHandler)
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("db error: sql is xxxxxxx")
		})
	})
	s.SetPort(8199)
	s.Run()
}

模板解析

  1. WriteTpl*方法用于模板输出,解析并输出模板文件,也可以直接解析并输出给定的模板内容。
  2. ParseTpl*方法用于模板解析,解析模板文件或者模板内容,返回解析后的内容。
内置变量

Config 访问默认的配置管理(config.toml)对象配置项。{{.Config.配置项}}

Cookie 访问当前请求的Cookie对象参数值 {{.Cookie.键名}}

Session 访问当前请求的Session对象参数值。{{.Session.键名}}

Query 访问当前Query String中的请求参数值。 {{.Query.键名}}

Form 访问当前表单请求参数值 {{.Form.键名}}

Request 访问当前请求参数值(不区分参数提交方式)。 {{.Request.键名}}

示例
package main

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

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Cookie.Set("theme", "default")
        r.Session.Set("name", "john")
        content :=`Config:{{.Config.redis.cache}}, Cookie:{{.Cookie.theme}}, Session:{{.Session.name}}, Query:{{.Query.name}}`
        r.Response.WriteTplContent(content, nil)
    })
    s.SetPort(8199)
    s.Run()
}
JSON/XML
  1. WriteJson* 方法用于返回JSON数据格式,参数为任意类型,可以为string、map、struct等等。返回的Content-Type为application/json。
  2. WriteXml* 方法用于返回XML数据格式,参数为任意类型,可以为string、map、struct等等。返回的Content-Type为application/xml。

JSON

package main

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

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/json", func(r *ghttp.Request) {
			r.Response.WriteJson(g.Map{
				"id":   1,
				"name": "john",
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

JSONP

package main

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

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/jsonp", func(r *ghttp.Request) {
			r.Response.WriteJsonP(g.Map{
				"id":   1,
				"name": "john",
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

XML

package main

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

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/xml", func(r *ghttp.Request) {
            r.Response.Write(`<?xml version="1.0" encoding="UTF-8"?>`)
			r.Response.WriteXml(g.Map{
				"id":   1,
				"name": "john",
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}
type Cookie
    func GetCookie(r *Request) *Cookie
    func (c *Cookie) Contains(key string) bool
    func (c *Cookie) Flush()
    func (c *Cookie) Get(key string, def ...string) string
    func (c *Cookie) GetSessionId() string
    func (c *Cookie) Map() map[string]string
    func (c *Cookie) Remove(key string)
    func (c *Cookie) RemoveCookie(key, domain, path string)
    func (c *Cookie) Set(key, value string)
    func (c *Cookie) SetCookie(key, value, domain, path string, maxAge time.Duration, httpOnly ...bool)
    func (c *Cookie) SetHttpCookie(httpCookie *http.Cookie)
    func (c *Cookie) SetSessionId(id string)
  1. Cookie.GetSessionId()用于获取当前请求提交的SessionId,每个请求的SessionId都是唯一的,并且伴随整个请求流程,该值可能为空。 2.Cookie.SetSessionId(id string)用于自定义设置SessionId到Cookie中,返回给客户端(往往是浏览器)存储,随后客户端每一次请求在Cookie中可带上该SessionId。

在设置Cookie变量的时候可以给定过期时间,该时间为可选参数,默认的Cookie过期时间为一年。

默认的SessionId在Cookie中的存储名称为gfsession。

示例
package main

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

func main() {
    s := g.Server()
    s.BindHandler("/cookie", func(r *ghttp.Request) {
        datetime := r.Cookie.Get("datetime")
        r.Cookie.Set("datetime", gtime.Datetime())
        r.Response.Write("datetime:", datetime)
    })
    s.SetPort(8199)
    s.Run()
}

通过SetCookie来设置Cookie键值对并将maxAge参数设置为0即可Cookie随着用户的浏览会话过期

r.Cookie.SetCookie(“MyCookieKey”, “MyCookieValue”, “”, “/”, 0)

Session

gsession模块
  1. gsession.Manager:管理Session对象、Storage持久化存储对象、以及过期时间控制。
  2. gsession.Session:单个Session会话管理对象,用于Session参数的增删查改等数据管理操作。
  3. gsession.Storage:这是一个接口定义,用于Session对象的持久化存储、数据写入/读取、存活更新等操作,开发者可基于该接口实现自定义的持久化存储特性。
存储实现方式
基于文件存储(默认):单节点部署方式下比较高效的持久化存储方式;
基于纯内存存储:性能最高效,但是无法持久化保存,重启即丢失;
基于Redis存储:远程Redis节点存储Session数据,支持应用多节点部署;

在默认情况下,ghttp.Server的Session存储使用了内存+文件的方式,使用StorageFile对象实现

文件存储示例:

package main

import (
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
	"github.com/gogf/gf/os/gtime"
	"time"
)

func main() {
	s := g.Server()
	s.SetConfigWithMap(g.Map{  //将Session的过期时间设置为1分钟
		"SessionMaxAge": time.Minute,
	})
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/set", func(r *ghttp.Request) {
			r.Session.Set("time", gtime.Timestamp())
			r.Response.Write("ok")
		})
		group.ALL("/get", func(r *ghttp.Request) {
			r.Response.Write(r.Session.Map())
		})
		group.ALL("/del", func(r *ghttp.Request) {
			r.Session.Clear()
			r.Response.Write("ok")
		})
	})
	s.SetPort(8199)
	s.Run()
}
  1. 首先,访问 http://127.0.0.1:8199/set 设置一个Session变量;
  2. 随后,访问 http://127.0.0.1:8199/get 可以看到该Session变量已经设置并成功获取;
  3. 接着,我们停止程序,并重新启动,再次访问 http://127.0.0.1:8199/get ,可以看到Session变量已经从文件存储中恢复;
  4. 等待1分钟后,再次访问 http://127.0.0.1:8199/get 可以看到已经无法获取该Session,因为该Session已经过期;

内存存储示例

package main

import (
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
	"github.com/gogf/gf/os/gsession"
	"github.com/gogf/gf/os/gtime"
	"time"
)

func main() {
	s := g.Server()
	s.SetConfigWithMap(g.Map{
		"SessionMaxAge":  time.Minute,
		"SessionStorage": gsession.NewStorageMemory(),
	})
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/set", func(r *ghttp.Request) {
			r.Session.Set("time", gtime.Timestamp())
			r.Response.Write("ok")
		})
		group.ALL("/get", func(r *ghttp.Request) {
			r.Response.Write(r.Session.Map())
		})
		group.ALL("/del", func(r *ghttp.Request) {
			r.Session.Clear()
			r.Response.Write("ok")
		})
	})
	s.SetPort(8199)
	s.Run()
}
  1. 首先,访问 http://127.0.0.1:8199/set 设置一个Session变量;
  2. 随后,访问 http://127.0.0.1:8199/get 可以看到该Session变量已经设置并成功获取;
  3. 接着,我们停止程序,并重新启动,再次访问 http://127.0.0.1:8199/get ,可以看到Session变量已经没有了;

Redis存储

与文件存储唯一不同的是,在每一次请求中如果需要对Session进行操作时,将会从Redis中拉取一次最新的Session数据(而文件存储只会在Session不存在时读取一次文件)。

package main

import (
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
	"github.com/gogf/gf/os/gsession"
	"github.com/gogf/gf/os/gtime"
	"time"
)

func main() {
	s := g.Server()
	s.SetConfigWithMap(g.Map{
		"SessionMaxAge":  time.Minute,
		"SessionStorage": gsession.NewStorageRedis(g.Redis()),
	})
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/set", func(r *ghttp.Request) {
			r.Session.Set("time", gtime.Timestamp())
			r.Response.Write("ok")
		})
		group.ALL("/get", func(r *ghttp.Request) {
			r.Response.Write(r.Session.Map())
		})
		group.ALL("/del", func(r *ghttp.Request) {
			r.Session.Clear()
			r.Response.Write("ok")
		})
	})
	s.SetPort(8199)
	s.Run()
}
  1. 首先,访问 http://127.0.0.1:8199/set 设置一个Session变量;
  2. 随后,访问 http://127.0.0.1:8199/get 可以看到该Session变量已经设置并成功获取;
  3. 接着,我们停止程序,并重新启动,再次访问 http://127.0.0.1:8199/get ,可以看到Session变量已经从Redis存储中恢复;如果我们手动修改Redis中的对应键值数据,页面刷新时也会读取到最新的值;
  4. 等待1分钟后,再次访问 http://127.0.0.1:8199/get 可以看到已经无法获取该Session,因为该Session已经过期;

服务配置

  1. 可以通过SetConfig及SetConfigWithMap来设置。
  2. 也可以使用Server对象的Set*/Enable*方法进行特定配置的设置。
  3. 主要注意的是,配置项在Server执行Start之后便不能再修改,防止产生并发安全问题。

SetConfigWithMap方法

不区分大小写,单词间也支持使用-/_/空格符号连接

s := g.Server()
s.SetConfigWithMap(g.Map{
    "address":          ":80",
    "serverRoot":       "/var/www/Server",
    "indexFiles":       g.Slice{"index.html", "main.html"},
    "accessLogEnabled": true,
    "errorLogEnabled":  true,
    "pprofEnabled":     true,
    "logPath":          "/var/log/ServerLog",
    "sessionIdName":    "MySessionId",
    "sessionPath":      "/tmp/MySessionStoragePath",
    "sessionMaxAge":    24 * time.Hour,
    "dumpRouterMap":    false,
})
s.Run()

多个配置项读取

[server]
    address    = ":80"
    serverRoot = "/var/www/Server"
    [server.server1]
        address    = ":8080"
        serverRoot = "/var/www/Server1"
    [server.server2]
        address    = ":8088"
        serverRoot = "/var/www/Server2"
// 对应 server.server1 配置项
s1 := g.Server("server1")
// 对应 server.server2 配置项
s2 := g.Server("server2")
// 对应默认配置项 server
s3 := g.Server("none")
// 对应默认配置项 server
s4 := g.Server()

上传限制

  1. MaxHeaderBytes:请求头大小限制,请求头包括客户端提交的Cookie数据,默认设置为10KB。
  2. ClientMaxBodySize:客户端提交的Body大小限制,同时也影响文件上传大小,默认设置为8MB。
[server]
    maxHeaderBytes    = "20KB"
    clientMaxBodySize = "200MB"

配置示例

所有的配置项请参考ServerConfig对象属性: https://godoc.org/github.com/gogf/gf/net/ghttp#ServerConfig

[server]
    # 基本配置
    address             = ":80"                        # 本地监听地址。默认":80"
	httpsAddr           = ":443"                       # TLS/HTTPS配置,同时需要配置证书和密钥。默认关闭
	httpsCertPath       = ""                           # TLS/HTTPS证书文件本地路径,建议使用绝对路径。默认关闭
	httpsKeyPath        = ""                           # TLS/HTTPS密钥文件本地路径,建议使用绝对路径。默认关闭
	readTimeout         = "60s"                        # 请求读取超时时间,一般不需要配置。默认为60秒
	writeTimeout        = "0"                          # 数据返回写入超时时间,一般不需要配置。默认不超时(0)
	idleTimeout         = "60s"                        # 仅当Keep-Alive开启时有效,请求闲置时间。默认为60秒
	maxHeaderBytes      = "10240"                      # 请求Header大小限制(Byte)。默认为10KB
	keepAlive           = true                         # 是否开启Keep-Alive功能。默认true
	serverAgent         = "GF HTTP Server"             # 服务端Agent信息。默认为"GF HTTP Server"

    # 静态服务配置
	indexFiles          = ["index.html","index.htm"]   # 自动首页静态文件检索。默认为["index.html", "index.htm"]
	indexFolder         = false                        # 当访问静态文件目录时,是否展示目录下的文件列表。默认关闭,那么请求将返回403
    serverRoot          = "/var/www"                   # 静态文件服务的目录根路径,配置时自动开启静态文件服务。默认关闭
	searchPaths         = ["/home/www","/var/lib/www"] # 提供静态文件服务时额外的文件搜索路径,当根路径找不到时则按照顺序在搜索目录查找。默认关闭
	fileServerEnabled   = false                        # 静态文件服务总开关。默认false

    # Cookie配置
	cookieMaxAge        = "365d"             # Cookie有效期。默认为365天
	cookiePath          = "/"                # Cookie有效路径。默认为"/"表示全站所有路径下有效
	cookieDomain        = ""                 # Cookie有效域名。默认为当前配置Cookie时的域名

	# Sessions配置
	sessionMaxAge       = "24h"              # Session有效期。默认为24小时
	sessionIdName       = "gfsessionid"      # SessionId的键名名称。默认为gfsessionid
	sessionCookieOutput = true               # Session特性开启时,是否将SessionId返回到Cookie中。默认true
	sessionPath         = "/tmp/gsessions"   # Session存储的文件目录路径。默认为当前系统临时目录下的gsessions目录

    # Logging配置
	logPath             = ""                 # 日志文件存储目录路径,建议使用绝对路径。默认为空,表示关闭
    logStdout           = true               # 日志是否输出到终端。默认为true
    errorStack          = true               # 当Server捕获到异常时是否记录堆栈信息到日志中。默认为true
    errorLogEnabled     = true               # 是否记录异常日志信息到日志中。默认为true
    errorLogPattern     = "error-{Ymd}.log"  # 异常错误日志文件格式。默认为"error-{Ymd}.log"
    accessLogEnabled    = false              # 是否记录访问日志。默认为false
    accessLogPattern    = "access-{Ymd}.log" # 访问日志文件格式。默认为"access-{Ymd}.log"

    # PProf配置
	pprofEnabled        = false              # 是否开启PProf性能调试特性。默认为false
	pprofPattern        = ""                 # 开启PProf时有效,表示PProf特性的页面访问路径,对当前Server绑定的所有域名有效。

    # 其他配置
	clientMaxBodySize   = 810241024          # 客户端最大Body上传限制大小,影响文件上传大小(Byte)。默认为8*1024*1024=8MB
	formParsingMemory   = 1048576            # 解析表单时的缓冲区大小(Byte),一般不需要配置。默认为1024*1024=1MB
	nameToUriType       = 0                  # 路由注册中使用对象注册时的路由生成规则。默认为0
	routeOverWrite      = false              # 当遇到重复路由注册时是否强制覆盖。默认为false,重复路由存在时将会在启动时报错退出
	dumpRouterMap       = true               # 是否在Server启动时打印所有的路由列表。默认为true
	graceful            = false              # 是否开启平滑重启特性,开启时将会在本地增加10000的本地TCP端口用于进程间通信。默认false

异常处理

这里使用一个全局的后置中间件来捕获异常

package main

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

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if err := r.GetError(); err != nil {
		// 记录到自定义错误日志文件
		g.Log("exception").Error(err)
		//返回固定的友好信息
		r.Response.ClearBuffer()
		r.Response.Writeln("服务器居然开小差了,请稍后再试吧!")
	}
}

func main() {
	s := g.Server()
	s.Use(MiddlewareErrorHandler)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("db error: sql is xxxxxxx")
		})
	})
	s.SetPort(8199)
	s.Run()
}

HTTPClient

HTTP客户端

type Client
    // 创建客户端
    func NewClient() *Client
    // 原始请求方法
    func (c *Client) Get(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Put(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Post(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Delete(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Connect(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Head(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Options(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Patch(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) Trace(url string, data ...interface{}) (*ClientResponse, error)
    func (c *Client) DoRequest(method, url string, data ...interface{}) (*ClientResponse, error)
    // 发起HTTP请求并返回二进制内容
    func (c *Client) GetBytes(url string, data ...interface{}) []byte
    func (c *Client) PutBytes(url string, data ...interface{}) []byte
    func (c *Client) PostBytes(url string, data ...interface{}) []byte
    func (c *Client) DeleteBytes(url string, data ...interface{}) []byte
    func (c *Client) ConnectBytes(url string, data ...interface{}) []byte
    func (c *Client) HeadBytes(url string, data ...interface{}) []byte
    func (c *Client) OptionsBytes(url string, data ...interface{}) []byte
    func (c *Client) PatchBytes(url string, data ...interface{}) []byte
    func (c *Client) TraceBytes(url string, data ...interface{}) []byte
    func (c *Client) RequestBytes(method string, url string, data ...interface{}) []byte
    // 发起HTTP请求并返回字符串内容
    func (c *Client) GetContent(url string, data ...interface{}) string
    func (c *Client) PutContent(url string, data ...interface{}) string
    func (c *Client) PostContent(url string, data ...interface{}) string
    func (c *Client) DeleteContent(url string, data ...interface{}) string
    func (c *Client) ConnectContent(url string, data ...interface{}) string
    func (c *Client) HeadContent(url string, data ...interface{}) string
    func (c *Client) OptionsContent(url string, data ...interface{}) string
    func (c *Client) PatchContent(url string, data ...interface{}) string
    func (c *Client) TraceContent(url string, data ...interface{}) string
    func (c *Client) RequestContent(method string, url string, data ...interface{}) string
    // 发起HTTP请求并返回泛型对象方便后续类型转换
    func (c *Client) GetVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) PutVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) PostVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) DeleteVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) HeadVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) PatchVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) ConnectVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) OptionsVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) TraceVar(url string, data ...interface{}) *gvar.Var
    func (c *Client) RequestVar(method string, url string, data ...interface{}) *gvar.Var
    // 链式操作
    func (c *Client) Ctx(ctx context.Context) *Client
    func (c *Client) Timeout(t time.Duration) *Client
    func (c *Client) Cookie(m map[string]string) *Client
    func (c *Client) Header(m map[string]string) *Client
    func (c *Client) HeaderRaw(headers string) *Client
    func (c *Client) ContentType(contentType string) *Client
    func (c *Client) ContentJson() *Client
    func (c *Client) ContentXml() *Client
    func (c *Client) BasicAuth(user, pass string) *Client
    func (c *Client) Retry(retryCount int, retryInterval time.Duration) *Client
    func (c *Client) Proxy(proxyURL string) *Client
    func (c *Client) RedirectLimit(redirectLimit int) *Client
    // 设置管理
    func (c *Client) SetBasicAuth(user, pass string) *Client
    func (c *Client) SetBrowserMode(enabled bool) *Client
    func (c *Client) SetContentType(contentType string) *Client
    func (c *Client) SetCookie(key, value string) *Client
    func (c *Client) SetCookieMap(m map[string]string) *Client
    func (c *Client) SetCtx(ctx context.Context) *Client
    func (c *Client) SetHeader(key, value string) *Client
    func (c *Client) SetHeaderMap(m map[string]string) *Client
    func (c *Client) SetHeaderRaw(headers string) *Client
    func (c *Client) SetPrefix(prefix string) *Client
    func (c *Client) SetRetry(retryCount int, retryInterval int) *Client
    func (c *Client) SetTimeout(t time.Duration) *Client
    func (c *Client) SetProxy(proxyURL string)

HTTP返回对象

func (r *ClientResponse) GetCookie(key string) string
func (r *ClientResponse) GetCookieMap() map[string]string
func (r *ClientResponse) Raw() string
func (r *ClientResponse) RawDump()
func (r *ClientResponse) RawRequest() string
func (r *ClientResponse) RawResponse() string
func (r *ClientResponse) ReadAll() []byte
func (r *ClientResponse) ReadAllString() string
func (r *ClientResponse) Close() error

HTTPClient-链式操作

示例1,请求超时控制
g.Client().Timeout(3*time.Second).GetContent("http://user.svc/v1/user/info/1")

g.Client().Timeout(10*time.Second).PostContent("http://order.svc/v1/order/create", g.Map{
    "uid"         : 1,
    "sku_id"      : 10000,
    "amount"      : 19.99,
    "create_time" : "2020-03-26 12:00:00",
})
示例2,自定义Cookie
g.Client().Cookie("sessionid", "MNV5432PIY76").GetContent("http://user.svc/v1/user/info/1")
示例3,自定义Header
g.Client().Header("Trace-Id", "XVF654RT98UJNMN641V06Y").GetContent("http://user.svc/v1/user/info/1")

g.Client().HeaderRaw(`
Referer: https://goframe.org/
User-Agent: MyTesyClient
`).GetContent("http://user.svc/v1/user/info")
示例4,提交Json请求
g.Client().ContentJson().PostContent("http://order.svc/v1/order/create", g.Map{
    "uid"         : 1,
    "sku_id"      : 10000,
    "amount"      : 19.99,
    "create_time" : "2020-03-26 12:00:00",
})
示例5,提交Xml请求
g.Client().ContentXml().PostContent("http://order.svc/v1/order/create", g.Map{
    "uid"         : 1,
    "sku_id"      : 10000,
    "amount"      : 19.99,
    "create_time" : "2020-03-26 12:00:00",
})

HTTPClient-基本使用

发送GET请求,并打印出返回值
if r, err := g.Client().Get("https://goframe.org"); err != nil {
    panic(err)
} else {
    defer r.Close()
    fmt.Println(r.ReadAllString())
}
发送GET请求,下载远程文件
if r, err := g.Client().Get("https://goframe.org/cover.png"); err != nil {
    panic(err)
} else {
    defer r.Close()
    gfile.PutBytes("/Users/john/Temp/cover.png", r.ReadAll())
}
发送POST请求,并打印出返回值
if r, err := g.Client().Post("http://127.0.0.1:8199/form", "name=john&age=18"); err != nil {
    panic(err)
} else {
    defer r.Close()
    fmt.Println(r.ReadAllString())
}
发送POST请求,参数为map类型,并打印出返回值
if r, err := g.Client().Post(
    "http://127.0.0.1:8199/form",
    g.Map{
        "submit"   : "1",
        "callback" : "http://127.0.0.1/callback?url=http://baidu.com",
    }
)); err != nil {
    panic(err)
} else {
    defer r.Close()
    fmt.Println(r.ReadAllString())
}
发送POST请求,参数为JSON数据,并打印出返回值
if r, err := g.Client().Post(
    "http://user.svc/v1/user/create",
    `{"passport":"john","password":"123456","password-confirm":"123456"}`,
); err != nil {
    panic(err)
} else {
    defer r.Close()
    fmt.Println(r.ReadAllString())
}
发送DELETE请求,并打印出返回值
if r, err := g.Client().Delete("http://user.svc/v1/user/delete/1", "10000"); err != nil {
    panic(err)
} else {
    defer r.Close()
    fmt.Println(r.ReadAllString())
}

Bytes及Content方法

以Bytes及Content后缀结尾的请求方法为直接获取返回内容的快捷方法,这些方法将会自动读取服务端返回内容并自动关闭请求连接

发送GET请求,返回服务端返回内容
 // 返回content为[]bytes类型
 content := g.Client().GetBytes("https://goframe.org")
发送POST请求,返回服务端返回内容
 // 返回content为[]bytes类型
 content := g.Client().PostBytes(
     "http://user.svc/v1/user/create", 
     `{"passport":"john","password":"123456","password-confirm":"123456"}`,
 )


  // 返回content为string类型
 content := g.Client().PostContent(
     "http://user.svc/v1/user/create", 
     `{"passport":"john","password":"123456","password-confirm":"123456"}`,
 )

*Var方法

以Var后缀结尾的请求方法直接请求并获取HTTP接口结果为泛型类型便于转换。

type User struct {
    Id   int
    Name string
}

var user *User
// 也可以
// var user = new(User)
g.Client().GetVar(url).Scan(&user)

// Struct数组
var users []*User
// 也可以
// var users []User
g.Client().GetVar(url).Scan(&users)

HTTPClient-文件上传

上面已说过,略

HTTPClient-自定义Cookie

func (c *Client) SetCookie(key, value string) *Client
func (c *Client) SetCookieMap(m map[string]string) *Client

服务端

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Write(r.Cookie.Map())
    })
    s.SetPort(8199)
    s.Run()
}

客户端

package main


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


func main() {
    c := g.Client()
    c.SetCookie("name", "john")
    c.SetCookie("score", "100")
    if r, e := c.Get("http://127.0.0.1:8199/"); e != nil {
        panic(e)
    } else {
        fmt.Println(r.ReadAllString())
    }
}

或者

package main


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


func main() {
    c := g.Client()
    c.SetCookieMap(g.MapStrStr{
        "name":  "john",
        "score": "100",
    })
    if r, e := c.Get("http://127.0.0.1:8199/"); e != nil {
        panic(e)
    } else {
        fmt.Println(r.ReadAllString())
    }
}

HTTPClient-自定义Header

func (c *Client) SetHeader(key, value string) *Client
func (c *Client) SetHeaderMap(m map[string]string) *Client
func (c *Client) SetHeaderRaw(headers string) *Client

服务端

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Writef(
			"Span-Id:%s,Trace-Id:%s",
			r.Header.Get("Span-Id"),
			r.Header.Get("Trace-Id"),
		)
	})
	s.SetPort(8199)
	s.Run()
}

客户端

package main

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


func main() {
    c := g.Client()
    c.SetHeader("Span-Id", "0.0.1")
    c.SetHeader("Trace-Id", "NBC56410N97LJ016FQA")
    if r, e := c.Get("http://127.0.0.1:8199/"); e != nil {
        panic(e)
    } else {
        fmt.Println(r.ReadAllString())
    }
}

package main


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


func main() {
    c := g.Client()
    c.SetHeaderRaw(`
        Referer: https://localhost
        Span-Id: 0.0.1
        Trace-Id: NBC56410N97LJ016FQA
        User-Agent: MyTestClient
    `)
    if r, e := c.Get("http://127.0.0.1:8199/"); e != nil {
        panic(e)
    } else {
        fmt.Println(r.ReadAllString())
    }
}

HTTPClient-请求信息打印

方便调试

func (r *ClientResponse) Raw() string
func (r *ClientResponse) RawDump()
func (r *ClientResponse) RawRequest() string
func (r *ClientResponse) RawResponse() string
package main

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

func main() {
	response, err := g.Client().Get("https://goframe.org")
	if err != nil {
		panic(err)
	}
	response.RawDump()
}

HTTPClient-代理Proxy设置

client := g.Client()
client.SetProxy("http://127.0.0.1:1081")
client.SetTimeout(5 * time.Second)
response, err := client.Get("https://api.ip.sb/ip")
if err != nil {
    fmt.Println(err)
}
response.RawDump()
client := g.Client()
response, err := client.Proxy("http://127.0.0.1:1081").Get("https://api.ip.sb/ip")
if err != nil {
    fmt.Println(err)
}
fmt.Println(response.RawResponse())

分页管理

import “github.com/gogf/gf/util/gpage”

文档:https://godoc.org/github.com/gogf/gf/util/gpage

分页管理-动态分页

通过GET参数(通过QueryString)传递分页参数,默认分页参数名称为page

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/page/demo", func(r *ghttp.Request) {
		page := r.GetPage(100, 10)  //总100条,每10条一页
		buffer, _ := gview.ParseContent(`
        <html>
            <head>
                <style>
                    a,span {padding:8px; font-size:16px;}
                    div{margin:5px 5px 20px 5px}
                </style>
            </head>
            <body>
                <div>{{.page1}}</div>
                <div>{{.page2}}</div>
                <div>{{.page3}}</div>
                <div>{{.page4}}</div>
            </body>
        </html>
        `, g.Map{
			"page1": page.GetContent(1),  //展现的样式
			"page2": page.GetContent(2),
			"page3": page.GetContent(3),
			"page4": page.GetContent(4),
		})
		r.Response.Write(buffer)
	})
	s.SetPort(8199)
	s.Run()
}

分页管理-静态分页

静态分页是指页面的分页参数使用的是路由传参.路由定义中需要给定一个page名称的路由参数,可以使用模糊匹配路由*page,也可以使用命名匹配路由:page,也可以使用字段匹配路由{page}。

示例1,使用模糊匹配路由
package main

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

func main() {
	s := g.Server()
	s.BindHandler("/page/static/*page", func(r *ghttp.Request) {
		page := r.GetPage(100, 10)
		buffer, _ := gview.ParseContent(`
        <html>
            <head>
                <style>
                    a,span {padding:8px; font-size:16px;}
                    div{margin:5px 5px 20px 5px}
                </style>
            </head>
            <body>
                <div>{{.page1}}</div>
                <div>{{.page2}}</div>
                <div>{{.page3}}</div>
                <div>{{.page4}}</div>
            </body>
        </html>
        `, g.Map{
			"page1": page.GetContent(1),
			"page2": page.GetContent(2),
			"page3": page.GetContent(3),
			"page4": page.GetContent(4),
		})
		r.Response.Write(buffer)
	})
	s.SetPort(8199)
	s.Run()
}
示例2,使用字段匹配路由

使用了{page}字段匹配规则,用于获取当前的分页页码信息

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/:obj/*action/{page}.html", func(r *ghttp.Request) {
		page := r.GetPage(100, 10)
		buffer, _ := gview.ParseContent(`
        <html>
            <head>
                <style>
                    a,span {padding:8px; font-size:16px;}
                    div{margin:5px 5px 20px 5px}
                </style>
            </head>
            <body>
                <div>{{.page1}}</div>
                <div>{{.page2}}</div>
                <div>{{.page3}}</div>
                <div>{{.page4}}</div>
            </body>
        </html>
        `, g.Map{
			"page1": page.GetContent(1),
			"page2": page.GetContent(2),
			"page3": page.GetContent(3),
			"page4": page.GetContent(4),
		})
		r.Response.Write(buffer)
	})
	s.SetPort(8199)
	s.Run()
}

分页管理-Ajax分页

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/page/ajax", func(r *ghttp.Request) {
		page := r.GetPage(100, 10)
		page.AjaxActionName = "DoAjax"
		buffer, _ := gview.ParseContent(`
        <html>
            <head>
                <style>
                    a,span {padding:8px; font-size:16px;}
                    div{margin:5px 5px 20px 5px}
                </style>
                <script src="https://cdn.bootcss.com/jquery/2.0.3/jquery.min.js"></script>
                <script>
                function DoAjax(url) {
                     $.get(url, function(data,status) {
                         $("body").html(data);
                     });
                }
                </script>
            </head>
            <body>
                <div>{{.page1}}</div>
                <div>{{.page2}}</div>
                <div>{{.page3}}</div>
                <div>{{.page4}}</div>
            </body>
        </html>
        `, g.Map{
			"page1": page.GetContent(1),
			"page2": page.GetContent(2),
			"page3": page.GetContent(3),
			"page4": page.GetContent(4),
		})
		r.Response.Write(buffer)
	})
	s.SetPort(8199)
	s.Run()
}

分页管理-URL模板

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/page/template/{page}.html", func(r *ghttp.Request) {
		page := r.GetPage(100, 10)
		page.UrlTemplate = "/order/list/{.page}.html"
		buffer, _ := gview.ParseContent(`
        <html>
            <head>
                <style>
                    a,span {padding:8px; font-size:16px;}
                    div{margin:5px 5px 20px 5px}
                </style>
            </head>
            <body>
                <div>{{.page1}}</div>
                <div>{{.page2}}</div>
                <div>{{.page3}}</div>
                <div>{{.page4}}</div>
            </body>
        </html>
        `, g.Map{
			"page1": page.GetContent(1),
			"page2": page.GetContent(2),
			"page3": page.GetContent(3),
			"page4": page.GetContent(4),
		})
		r.Response.Write(buffer)
	})
	s.SetPort(8199)
	s.Run()
}

分页管理-自定义分页

自定义标签替换
package main

import (
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
	"github.com/gogf/gf/os/gview"
	"github.com/gogf/gf/text/gstr"
	"github.com/gogf/gf/util/gpage"
)

// wrapContent wraps each of the page tag with html li and ul.
func wrapContent(page *gpage.Page) string {
	content := page.GetContent(4)
	content = gstr.ReplaceByMap(content, map[string]string{
		"<span":  "<li><span",
		"/span>": "/span></li>",
		"<a":     "<li><a",
		"/a>":    "/a></li>",
	})
	return "<ul>" + content + "</ul>"
}

func main() {
	s := g.Server()
	s.BindHandler("/page/custom1/*page", func(r *ghttp.Request) {
		page := r.GetPage(100, 10)
		content := wrapContent(page)
		buffer, _ := gview.ParseContent(`
        <html>
            <head>
                <style>
                    a,span {padding:8px; font-size:16px;}
                    div{margin:5px 5px 20px 5px}
                </style>
            </head>
            <body>
                <div>{{.page}}</div>
            </body>
        </html>
        `, g.Map{
			"page": content,
		})
		r.Response.Write(buffer)
	})
	s.SetPort(10000)
	s.Run()
}
定义分页标签名称
package main

import (
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
	"github.com/gogf/gf/os/gview"
	"github.com/gogf/gf/util/gpage"
)

// pageContent customizes the page tag name.
func pageContent(page *gpage.Page) string {
	page.NextPageTag = "NextPage"
	page.PrevPageTag = "PrevPage"
	page.FirstPageTag = "HomePage"
	page.LastPageTag = "LastPage"
	pageStr := page.FirstPage()
	pageStr += page.PrevPage()
	pageStr += page.PageBar()
	pageStr += page.NextPage()
	pageStr += page.LastPage()
	return pageStr
}

func main() {
	s := g.Server()
	s.BindHandler("/page/custom2/*page", func(r *ghttp.Request) {
		page := r.GetPage(100, 10)
		buffer, _ := gview.ParseContent(`
        <html>
            <head>
                <style>
                    a,span {padding:8px; font-size:16px;}
                    div{margin:5px 5px 20px 5px}
                </style>
            </head>
            <body>
                <div>{{.page}}</div>
            </body>
        </html>
        `, g.Map{
			"page": pageContent(page),
		})
		r.Response.Write(buffer)
	})
	s.SetPort(10000)
	s.Run()
}

高级特性

静态文件服务

默认情况下,gf Server关闭了静态文件服务的功能,如果开发者配置了静态文件目录,那么静态文件服务将会自动开启。

/ 设置http server参数 - ServerRoot
func (s *Server) SetServerRoot(root string)

// 添加静态文件搜索目录,必须给定目录的绝对路径
func (s *Server) AddSearchPath(path string)

// 设置http server参数 - IndexFiles,默认展示文件,如:index.html, index.htm
func (s *Server) SetIndexFiles(index []string)

// 是否允许展示访问目录的文件列表
func (s *Server) SetIndexFolder(enabled bool)

// 添加URI与静态目录的映射
func (s *Server) AddStaticPath(prefix string, path string)

// 静态文件服务总开关:是否开启/关闭静态文件服务
func (s *Server) SetFileServerEnabled(enabled bool)

// 设置URI重写规则
func (s *Server) SetRewrite(uri string, rewrite string)

// 设置URI重写规则(批量)
func (s *Server) SetRewriteMap(rewrites map[string]string)
示例1, 基本使用
package main

import "github.com/gogf/gf/frame/g"

// 静态文件服务器基本使用
func main() {
    s := g.Server()
    s.SetIndexFolder(true)
    s.SetServerRoot("/Users/john/Temp")
    s.AddSearchPath("/Users/john/Documents")
    s.SetPort(8199)
    s.Run()
}
示例2,静态目录映射
package main

import "github.com/gogf/gf/frame/g"

// 静态文件服务器,支持自定义静态目录映射
func main() {
    s := g.Server()
    s.SetIndexFolder(true)
    s.SetServerRoot("/Users/john/Temp")
    s.AddSearchPath("/Users/john/Documents")
    s.AddStaticPath("/my-doc", "/Users/john/Documents")
    s.SetPort(8199)
    s.Run()
}
示例3,静态目录映射,优先级控制
package main

import "github.com/gogf/gf/frame/g"

// 静态文件服务器,支持自定义静态目录映射
func main() {
    s := g.Server()
    s.SetIndexFolder(true)
    s.SetServerRoot("/Users/john/Temp")
    s.AddSearchPath("/Users/john/Documents")
    s.AddStaticPath("/my-doc", "/Users/john/Documents")
    s.AddStaticPath("/my-doc/test", "/Users/john/Temp")
    s.SetPort(8199)
    s.Run()
}
示例4,URI重写
package main

import "github.com/gogf/gf/frame/g"

func main() {
    s := g.Server()
    s.SetServerRoot("/Users/john/Temp")
    s.SetRewrite("/test.html", "/test1.html")
        s.SetRewriteMap(g.MapStrStr{
            "/my-test1" : "/test1.html",
            "/my-test2" : "/test2.html",
        })
    s.SetPort(8199)
    s.Run()
}
  1. 当我们访问 /test.html ,其实最终被重写到了 test1.html,返回的是该文件内容;
  2. 当我们访问 /my-test1 ,其实最终被重写到了 test1.html,返回的是该文件内容;
  3. 当我们访问 /my-test2 ,其实最终被重写到了 test2.html,返回的是该文件内容;

服务日志管理

日志配置

Logger            *glog.Logger      // Logger for server.
LogPath           string            // Directory for storing logging files.
LogStdout         bool              // Printing logging content to stdout.
ErrorStack        bool              // Logging stack information when error.
ErrorLogEnabled   bool              // Enable error logging files.
ErrorLogPattern   string            // Error log file pattern like: error-{Ymd}.log
AccessLogEnabled  bool              // Enable access logging files.
AccessLogPattern  string            // Error log file pattern like: access-{Ymd}.log
  1. 默认情况下,日志不会输出到文件中,而是直接打印到终端。默认情况下的access日志终端输出是关闭的,仅有error日志默认开启
  2. 所有的选项均可通过Server.Set方法设置,大部分选项可以通过Server.Get方法获取。
  3. LogPath属性用于设置日志目录,只有在设置了日志目录的情况下才会输出日志到日志文件中。
  4. ErrorLogPattern及AccessLogPattern用于配置日志文件名称格式,默认为error-{Ymd}.log及access-{Ymd}.log,例如:error-20191212.log, access-20191212.log。
配置文件
[server]
	LogPath           = "/var/log/gf-demos/server"
    LogStdout         = false
    ErrorStack        = true
    ErrorLogEnabled   = true
    ErrorLogPattern   = "error.{Ymd}.log"
    AccessLogEnabled  = true
    AccessLogPattern  = "access.{Ymd}.log"

HTTPS & TLS

示例代码

package main

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

func main() {
    s := ghttp.GetServer()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("来自于HTTPS的:哈喽世界!")
    })
    s.EnableHTTPS("/home/john/https/server.crt", "/home/john/https/server.key")
    s.SetPort(8199) //也可以通过s.SetHTTPSPort(8199)
    s.Run()
}

同时支持HTTPS及HTTP访问

package main

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

func main() {
    s := ghttp.GetServer()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("您可以同时通过HTTP和HTTPS方式看到该内容!")
    })
    s.EnableHTTPS("/home/john/https/server.crt", "/home/john/https/server.key")
    s.SetHTTPSPort(443)
    s.SetPort(80)
    s.Run()
}

平滑重启

目前平滑重启特性需要在本地打开一个10000端口的tcp监听服务。

默认情况下平滑重启特性是关闭的,可以通过配置选项打开

func (s *Server) Restart(newExeFilePath...string) error  //重启服务 
Restart的参数可指定自定义重启的可执行文件路径(newExeFilePath),不传递时默认为原可执行文件路径。

func (s *Server) Shutdown() error //关闭服务

func (s *Server) EnableAdmin(pattern ...string) //将管理页面注册到指定的路由规则上,默认地址是/debug/admin

示例1:基本使用

package main

import (
    "time"
    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/os/gproc"
    "github.com/gogf/gf/net/ghttp"
)

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("哈喽!")
    })
    s.BindHandler("/pid", func(r *ghttp.Request){
        r.Response.Writeln(gproc.Pid())
    })
    s.BindHandler("/sleep", func(r *ghttp.Request){
        r.Response.Writeln(gproc.Pid())
        time.Sleep(10*time.Second)
        r.Response.Writeln(gproc.Pid())
    })
    s.EnableAdmin()
    s.SetPort(8199)
    s.Run()
}

访问 http://127.0.0.1:8199/debug/admin ,这是s.EnableAdmin后默认注册的一个WebServer管理页面

示例2:HTTPS支持

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("哈罗!")
    })
    s.EnableHTTPS("/home/john/temp/server.crt", "/home/john/temp/server.key")
    s.EnableAdmin()
    s.SetPort(8200)
    s.Run()
}
  1. 访问 https://127.0.0.1:8200/debug/admin/restart 平滑重启HTTPS服务;
  2. 访问 https://127.0.0.1:8200/debug/admin/shutdown 平滑关闭WebServer服务;

示例3:多服务及多端口

package main

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

func main() {
    s1 := g.Server("s1")
    s1.EnableAdmin()
    s1.SetPort(8100, 8200)
    s1.Start()

    s2 := g.Server("s2")
    s2.EnableAdmin()
    s2.SetPort(8300, 8400)
    s2.Start()

    g.Wait()
}

命令行管理

kill -SIGUSR1 进程ID //重启服务

kill -SIGTERM 进程ID //关闭服务

CORS跨域处理

CORS配置

限制Origin来源

// 允许跨域请求中间件
func Middleware(r *ghttp.Request) {
	corsOptions := r.Response.DefaultCORSOptions()
	corsOptions.AllowDomain = []string{"goframe.org", "johng.cn"}
	r.Response.CORS(corsOptions)
	r.Middleware.Next()
}

OPTIONS请求

有的客户端,部分浏览器在发送AJAX请求之前会首先发送OPTIONS预请求检测后续请求是否允许发送。

package main

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

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()  //通过调用CORSDefault方法使用默认的跨域设置允许跨域请求。
	r.Middleware.Next()
}

func Order(r *ghttp.Request) {
	r.Response.Write("GET")
}

func main() {
	s := g.Server()
	s.Group("/api.v1", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS)
		group.GET("/order", Order)
	})
	s.SetPort(8199)
	s.Run()
}

授权跨域Origin

package main

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

func MiddlewareCORS(r *ghttp.Request) {
	corsOptions := r.Response.DefaultCORSOptions()
	corsOptions.AllowDomain = []string{"goframe.org", "baidu.com"}
	r.Response.CORS(corsOptions)
	r.Middleware.Next()
}

func Order(r *ghttp.Request) {
	r.Response.Write("GET")
}

func main() {
	s := g.Server()
	s.Group("/api.v1", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS)
		group.GET("/order", Order)
	})
	s.SetPort(8199)
	s.Run()
}

自定义检测授权

以下示例中,仅允许来自goframe.org域名的跨域请求,而来自其他域名的请求将会失败并返回403

package main

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

func MiddlewareCORS(r *ghttp.Request) {
	corsOptions := r.Response.DefaultCORSOptions()
	corsOptions.AllowDomain = []string{"goframe.org"}
	if !r.Response.CORSAllowedOrigin(corsOptions) {
		r.Response.WriteStatus(http.StatusForbidden)
		return
	}
	r.Response.CORS(corsOptions)
	r.Middleware.Next()
}

func Order(r *ghttp.Request) {
	r.Response.Write("GET")
}

func main() {
	s := g.Server()
	s.Group("/api.v1", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS)
		group.GET("/order", Order)
	})
	s.SetPort(8199)
	s.Run()
}

CSRF防御设置

通过中间件完成

import “github.com/gogf/csrf”

s := g.Server()
s.Group("/api.v2", func(group *ghttp.RouterGroup) {
	group.Middleware(csrf.NewWithCfg(csrf.Config{
		Cookie: &http.Cookie{
			Name: "_csrf",// token name in cookie
		},
		ExpireTime:      time.Hour * 24,
		TokenLength:     32,
		TokenRequestKey: "X-Token",// use this key to read token in request param
	}))
	group.ALL("/csrf", func(r *ghttp.Request) {
		r.Response.Writeln(r.Method + ": " + r.RequestURI)
	})
})

前端对接

package main

import (
	"net/http"
	"time"

	"github.com/gogf/csrf"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)

// default cfg
func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(csrf.New())
		group.ALL("/csrf", func(r *ghttp.Request) {
			r.Response.Writeln(r.Method + ": " + r.RequestURI)
		})
	})
	s.SetPort(8199)
	s.Run()
}

HOOK事件回调

ghttp.Server提供了事件回调注册功能,类似于其他框架的中间件功能,相比较于中间件,事件回调的特性更加简单。

func (s *Server) BindHookHandler(pattern string, hook string, handler HandlerFunc) error
func (s *Server) BindHookHandlerByMap(pattern string, hookmap map[string]HandlerFunc) error

支持的Hook事件列表:

ghttp.HOOK_BEFORE_SERVE     在进入/初始化服务对象之前,该事件是最常用的事件,特别是针对于权限控制、跨域请求等处理。

ghttp.HOOK_AFTER_SERVE    在完成服务执行流程之后。

ghttp.HOOK_BEFORE_OUTPUT    向客户端输出返回内容之前。

ghttp.HOOK_AFTER_OUTPUT    向客户端输出返回内容之后。
package main

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

func main() {
    // 基本事件回调使用
    p := "/:name/info/{uid}"
    s := g.Server()
    s.BindHookHandlerByMap(p, map[string]ghttp.HandlerFunc{
        ghttp.HOOK_BEFORE_SERVE  : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_BEFORE_SERVE) },
        ghttp.HOOK_AFTER_SERVE   : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_AFTER_SERVE) },
        ghttp.HOOK_BEFORE_OUTPUT : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_BEFORE_OUTPUT) },
        ghttp.HOOK_AFTER_OUTPUT  : func(r *ghttp.Request){ glog.Println(ghttp.HOOK_AFTER_OUTPUT) },
    })
    s.BindHandler(p, func(r *ghttp.Request) {
       r.Response.Write("用户:", r.Get("name"), ", uid:", r.Get("uid"))
    })
    s.SetPort(8199)
    s.Run()
}

相同事件注册

package main

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

// 优先调用的HOOK
func beforeServeHook1(r *ghttp.Request) {
	r.SetParam("name", "GoFrame")
	r.Response.Writeln("set name")
}

// 随后调用的HOOK
func beforeServeHook2(r *ghttp.Request) {
	r.SetParam("site", "https://goframe.org")
	r.Response.Writeln("set site")
}

// 允许对同一个路由同一个事件注册多个回调函数,按照注册顺序进行优先级调用。
// 为便于在路由表中对比查看优先级,这里讲HOOK回调函数单独定义为了两个函数。
func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Writeln(r.Get("name"))
		r.Response.Writeln(r.Get("site"))
	})
	s.BindHookHandler("/", ghttp.HOOK_BEFORE_SERVE, beforeServeHook1)
	s.BindHookHandler("/", ghttp.HOOK_BEFORE_SERVE, beforeServeHook2)
	s.SetPort(8199)
	s.Run()
}

改变业务逻辑

package main

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

func main() {
	s := g.Server()
	// 多事件回调示例,事件1
	pattern1 := "/:name/info"
	s.BindHookHandlerByMap(pattern1, map[string]ghttp.HandlerFunc{
		ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
			r.SetParam("uid", 1000)
		},
	})
	s.BindHandler(pattern1, func(r *ghttp.Request) {
		r.Response.Write("用户:", r.Get("name"), ", uid:", r.Get("uid"))
	})

	// 多事件回调示例,事件2
	pattern2 := "/{object}/list/{page}.java"
	s.BindHookHandlerByMap(pattern2, map[string]ghttp.HandlerFunc{
		ghttp.HOOK_BEFORE_OUTPUT: func(r *ghttp.Request) {
			r.Response.SetBuffer([]byte(
				fmt.Sprintf("通过事件修改输出内容, object:%s, page:%s", r.Get("object"), r.GetRouterString("page"))),
			)
		},
	})
	s.BindHandler(pattern2, func(r *ghttp.Request) {
		r.Response.Write(r.Router.Uri)
	})
	s.SetPort(8199)
	s.Run()
}

允许跨域请求

package main

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

func Order(r *ghttp.Request) {
	r.Response.Write("GET")
}

func main() {
	s := g.Server()
	s.Group("/api.v1", func(group *ghttp.RouterGroup) {
		group.Hook("/*any", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
			r.Response.CORSDefault()
		})
		group.GET("/order", Order)
	})
	s.SetPort(8199)
	s.Run()
}

WebSocket服务

HTML5客户端
<!DOCTYPE html>
<html>
<head>
    <title>gf websocket echo server</title>
    <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
<div class="container">
    <div class="list-group" id="divShow"></div>
    <div>
        <div><input class="form-control" id="txtContent" autofocus rows="6" placeholder="请输入发送内容"></div>
        <div><button class="btn btn-default" id="btnSend" style="margin-top:15px">发 送</button></div>
    </div>
</div>
</body>
</html>

<script type="application/javascript">
    // 显示提示信息
    function showInfo(content) {
        $("<div class=\"list-group-item list-group-item-info\">" + content + "</div>").appendTo("#divShow")
    }
    // 显示警告信息
    function showWaring(content) {
        $("<div class=\"list-group-item list-group-item-warning\">" + content + "</div>").appendTo("#divShow")
    }
    // 显示成功信息
    function showSuccess(content) {
        $("<div class=\"list-group-item list-group-item-success\">" + content + "</div>").appendTo("#divShow")
    }
    // 显示错误信息
    function showError(content) {
        $("<div class=\"list-group-item list-group-item-danger\">" + content + "</div>").appendTo("#divShow")
    }

    $(function () {
        var url = "ws://127.0.0.1:8199/ws";
        var ws  = new WebSocket(url);
        try {
            // ws连接成功
            ws.onopen = function () {
                showInfo("WebSocket Server [" + url +"] 连接成功!");
            };
            // ws连接关闭
            ws.onclose = function () {
                if (ws) {
                    ws.close();
                    ws = null;
                }
                showError("WebSocket Server [" + url +"] 连接关闭!");
            };
            // ws连接错误
            ws.onerror = function () {
                if (ws) {
                    ws.close();
                    ws = null;
                }
                showError("WebSocket Server [" + url +"] 连接关闭!");
            };
            // ws数据返回处理
            ws.onmessage = function (result) {
                showWaring(" > " + result.data);
            };
        } catch (e) {
            alert(e.message);
        }

        // 按钮点击发送数据
        $("#btnSend").on("click", function () {
            if (ws == null) {
                showError("WebSocket Server [" + url +"] 连接失败,请F5刷新页面!");
                return;
            }
            var content = $.trim($("#txtContent").val()).replace("/[\n]/g", "");
            if (content.length <= 0) {
                alert("请输入发送内容!");
                return;
            }
            $("#txtContent").val("")
            showSuccess(content);
            ws.send(content);
        });

        // 回车按钮触发发送点击事件
        $("#txtContent").on("keydown", function (event) {
            if (event.keyCode == 13) {
                $("#btnSend").trigger("click");
            }
        });
    })

</script>
WebSocket服务端
package main

import (
    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/net/ghttp"
    "github.com/gogf/gf/os/gfile"
    "github.com/gogf/gf/os/glog"
)

func main() {
    s := g.Server()
    s.BindHandler("/ws", func(r *ghttp.Request) {
        ws, err := r.WebSocket()
        if err != nil {
            glog.Error(err)
            r.Exit()
        }
        for {
            msgType, msg, err := ws.ReadMessage()
            if err != nil {
                return
            }
            if err = ws.WriteMessage(msgType, msg); err != nil {
                return
            }
        }
    })
    s.SetServerRoot(gfile.MainPkgPath())
    s.SetPort(8199)
    s.Run()
}

自定义状态码处理

package main

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

func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("halo 世界!")
    })
    s.BindStatusHandler(404, func(r *ghttp.Request){
        r.Response.Writeln("This is customized 404 page")
    })
    s.SetPort(8199)
    s.Run()
}
package main

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

func main() {
    s := g.Server()
    s.BindHandler("/status/:status", func(r *ghttp.Request) {
        r.Response.Write("woops, status ", r.Get("status"), " found")
    })
    s.BindStatusHandler(404, func(r *ghttp.Request){
        r.Response.RedirectTo("/status/404")
    })
    s.SetPort(8199)
    s.Run()
}

网络服务开发

TCP组件

import “github.com/gogf/gf/net/gtcp”

简单的echo服务器来演示TCPServer的使用

package main

import (
    "fmt"
    "github.com/gogf/gf/net/gtcp"
)

func main() {
    gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) {
        defer conn.Close()
        for {
            data, err := conn.Recv(-1) //Recv方法会通过阻塞方式接收数据
            if len(data) > 0 {
                if err := conn.Send(append([]byte("> "), data...)); err != nil {
                  fmt.Println(err)
                }
            }
            if err != nil {
                break
            }
        }
    }).Run()
}
TCP组件-连接对象

import “github.com/gogf/gf/net/gtcp”

示例1,简单使用

package main

import (
    "fmt"
    "time"
    "github.com/gogf/gf/net/gtcp"
    "github.com/gogf/gf/os/glog"
    "github.com/gogf/gf/util/gconv"
)

func main() {
    // Server
    go gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) {
        defer conn.Close()
        for {
            data, err := conn.Recv(-1)
            if len(data) > 0 {
                  fmt.Println(string(data))
            }
            if err != nil {
                break
            }
        }
    }).Run()

    time.Sleep(time.Second)

    // Client
    conn, err := gtcp.NewConn("127.0.0.1:8999")
    if err != nil {
        panic(err)
    }
    for i := 0; i < 10000; i++ {
        if err := conn.Send([]byte(gconv.String(i))); err != nil {
            glog.Error(err)
        }
        time.Sleep(time.Second)
    }
}

示例2,回显服务

package main

import (
    "fmt"
    "time"
    "github.com/gogf/gf/net/gtcp"
    "github.com/gogf/gf/os/glog"
    "github.com/gogf/gf/os/gtime"
)

func main() {
    // Server
    go gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) {
        defer conn.Close()
        for {
            data, err := conn.Recv(-1)
            if len(data) > 0 {
                if err := conn.Send(append([]byte("> "), data...)); err != nil {
                  fmt.Println(err)
                }
            }
            if err != nil {
                break
            }
        }
    }).Run()

    time.Sleep(time.Second)

    // Client
    for {
       if conn, err := gtcp.NewConn("127.0.0.1:8999"); err == nil {
           if b, err := conn.SendRecv([]byte(gtime.Datetime()), -1); err == nil {
               fmt.Println(string(b), conn.LocalAddr(), conn.RemoteAddr())
           } else {
               fmt.Println(err)
           }
           conn.Close()
       } else {
           glog.Error(err)
       }
       time.Sleep(time.Second)
    }
}

示例3,HTTP客户端

package main

import (
    "fmt"
    "bytes"
    "github.com/gogf/gf/net/gtcp"
    "github.com/gogf/gf/util/gconv"
)

func main() {
    conn, err := gtcp.NewConn("www.baidu.com:80")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    if err := conn.Send([]byte("GET / HTTP/1.1\n\n")); err != nil {
        panic(err)
    }

    header        := make([]byte, 0)
    content       := make([]byte, 0)
    contentLength := 0
    for {
        data, err := conn.RecvLine()
        // header读取,解析文本长度
        if len(data) > 0 {
            array := bytes.Split(data, []byte(": "))
            // 获得页面内容长度
            if contentLength == 0 && len(array) == 2 && bytes.EqualFold([]byte("Content-Length"), array[0]) {
                contentLength = gconv.Int(array[1])
            }
            header = append(header, data...)
            header = append(header, '\n')
        }
        // header读取完毕,读取文本内容
        if contentLength > 0 && len(data) == 0 {
            content, _ = conn.Recv(contentLength)
            break
        }
        if err != nil {
            fmt.Errorf("ERROR: %s\n", err.Error())
            break
        }
    }

    if len(header) > 0 {
        fmt.Println(string(header))
    }
    if len(content) > 0 {
        fmt.Println(string(content))
    }
}

核心组件

调试模式

  1. 命令行启动参数 - gf.debug=true
  2. 指定的环境变量 - GF_DEBUG=true
  3. 程序启动boot包中使用g.SetDebug方法手动打开/关闭

配置管理

GF的配置管理由gcfg模块实现,gcfg模块是并发安全的,仅提供配置文件读取功能,不提供数据写入/修改功能,支持的数据文件格式包括: JSON、XML、YAML/YML、TOML、INI。当配置文件在外部被修改后,配置管理器能够即时地刷新配置文件的缓存内容。

import “github.com/gogf/gf/os/gcfg”

默认读取的配置文件为config.toml

// 设置默认配置文件,默认的 config.toml 将会被覆盖
g.Cfg().SetFileName("config.json")
// 后续读取时将会读取到 config.json 配置文件内容,
g.Cfg().Get("database")
默认文件修改

通过单例模式

g.Cfg().SetFileName(“config.prod.toml”)

通过命令行启动参数

./main –gf.gcfg.file=config.prod.toml

通过环境变量(常用在容器中)

启动时修改环境变量: GF_GCFG_FILE=config.prod.toml; ./main

使用genv模块来修改环境变量:genv.Set(“GF_GCFG_FILE”, “config.prod.toml”)

配置读取

cfg包最大的特点是支持按层级获取配置数据,层级访问默认通过英文”.“号指定

// /home/www/templates/
g.Cfg().Get("viewpath")

// 127.0.0.1:6379,1
g.Cfg().Get("redis.cache")

// test2
g.Cfg().Get("database.default.1.name")
配置修改

func (c *Config) Set(pattern string, value interface{}) error

配置管理-TOML格式

跳转到这里学习

资源管理

指可以将任意文件/目录打包为Golang源码文件,并且编译到可执行文件中,随着可执行文件发布。

import “github.com/gogf/gf/os/gres”

资源管理-工具打包

gf pack -h

将项目的config,public,template三个目录的文件打包到Go文件,打包命令为:

gf pack config,public,template packed/data.go -n packed

使用打包的Go文件

1) 在boot包中优先引入packed资源包

import (
    _ "my-app/packed"

    // 其他包
)

2) 在main包中优先引入boot包

import (
    _ "my-app/boot"

    // 其他包
)

打印资源管理文件列表

以通过gres.Dump()方法打印出当前资源管理器中所有的文件列表

资源管理-方法操作

自定义打包示例

项目根目录下的public和config目录打包为data.bin二进制文件,并通过gaes加密算法对生成的二进制内容进行加密。

package main

import (
	"github.com/gogf/gf/crypto/gaes"
	"github.com/gogf/gf/os/gfile"
	"github.com/gogf/gf/os/gres"
)

var (
	CryptoKey = []byte("x76cgqt36i9c863bzmotuf8626dxiwu0")
)

func main() {
	binContent, err := gres.Pack("public,config")
	if err != nil {
		panic(err)
	}
	binContent, err = gaes.Encrypt(binContent, CryptoKey)
	if err != nil {
		panic(err)
	}
	if err := gfile.PutBytes("data.bin", binContent); err != nil {
		panic(err)
	}
}
自定义解包示例
package main

import (
	"github.com/gogf/gf/crypto/gaes"
	"github.com/gogf/gf/os/gfile"
	"github.com/gogf/gf/os/gres"
)

var (
	CryptoKey = []byte("x76cgqt36i9c863bzmotuf8626dxiwu0")
)

func main() {
	binContent := gfile.GetBytes("data.bin")
	binContent, err := gaes.Decrypt(binContent, CryptoKey)
	if err != nil {
		panic(err)
	}
	if err := gres.Add(binContent); err != nil {
		panic(err)
	}
	gres.Dump()
}
资源管理-使用示例

资源文件源码:https://github.com/gogf/gf/tree/master/os/gres/testdata/example/files

资源文件打包:https://github.com/gogf/gf/tree/master/os/gres/testdata/example/boot

日志组件

import “github.com/gogf/gf/os/glog”

日志组件-配置管理

配置文件

[logger]
	path                 = "/var/log/"   # 日志文件路径。默认为空,表示关闭,仅输出到终端
	file                 = "{Y-m-d}.log" # 日志文件格式。默认为"{Y-m-d}.log"
	prefix               = ""            # 日志内容输出前缀。默认为空    level                = "all"         # 日志输出级别
	ctxKeys              = []            # Context上下文变量名称,自动打印Context的变量到日志中。默认为空
	headerPrint          = true          # 是否打印日志的头信息。默认true
	stdoutPrint          = true          # 日志是否同时输出到终端。默认true
	rotateSize           = 0             # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性
	rotateExpire         = 0             # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性
	rotateBackupLimit    = 0             # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除
	rotateBackupExpire   = 0             # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除
	rotateBackupCompress = 0             # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩
	rotateCheckInterval  = "1h"          # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时

默认配置

[logger]
    path   = "/var/log"
    level  = "all"   //DEBU < INFO < NOTI < WARN < ERRO < CRIT,也支持ALL, DEV, PROD常见部署模式配置名称
    stdout = false

多个配置项

[logger]
    path   = "/var/log"
    level  = "all"
    stdout = false
    [logger.logger1]
        path   = "/var/log/logger1"
        level  = "dev"
        stdout = false
    [logger.logger2]
        path   = "/var/log/logger2"
        level  = "prod"
        stdout = true

可以通过单例对象名称获取对应配置的Logger单例对象:

// 对应 logger.logger1 配置项
l1 := g.Log("logger1")
// 对应 logger.logger2 配置项
l2 := g.Log("logger2")
// 对应默认配置项 logger
l3 := g.Log("none")
// 对应默认配置项 logger
l4 := g.Log()
日志组件-异步输出
package main

import (
	"github.com/gogf/gf/os/glog"
	"time"
)

func main() {
	glog.SetAsync(true)
	for i := 0; i < 10; i++ {
		glog.Async().Print("async log", i)
	}
	time.Sleep(time.Second)  //避免日志未输出
}

Async链式操作

package main

import (
	"github.com/gogf/gf/os/glog"
	"time"
)

func main() {
	for i := 0; i < 10; i++ {
		glog.Async().Print("async log", i)
	}
	time.Sleep(time.Second)
}
日志组件-Json格式
package main

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

func main() {
	glog.Debug(g.Map{"uid" : 100, "name" : "john"})
	type User struct {
		Uid  int    `json:"uid"`
		Name string `json:"name"`
	}
	glog.Debug(User{100, "john"})
}

类型转换

import “github.com/gogf/gf/util/gconv”

package main

import (
    "fmt"
    "github.com/gogf/gf/util/gconv"
)

func main() {
    i := 123.456
    fmt.Printf("%10s %v\n", "Int:",        gconv.Int(i))
    fmt.Printf("%10s %v\n", "Int8:",       gconv.Int8(i))
    fmt.Printf("%10s %v\n", "Int16:",      gconv.Int16(i))
    fmt.Printf("%10s %v\n", "Int32:",      gconv.Int32(i))
    fmt.Printf("%10s %v\n", "Int64:",      gconv.Int64(i))
    fmt.Printf("%10s %v\n", "Uint:",       gconv.Uint(i))
    fmt.Printf("%10s %v\n", "Uint8:",      gconv.Uint8(i))
    fmt.Printf("%10s %v\n", "Uint16:",     gconv.Uint16(i))
    fmt.Printf("%10s %v\n", "Uint32:",     gconv.Uint32(i))
    fmt.Printf("%10s %v\n", "Uint64:",     gconv.Uint64(i))
    fmt.Printf("%10s %v\n", "Float32:",    gconv.Float32(i))
    fmt.Printf("%10s %v\n", "Float64:",    gconv.Float64(i))
    fmt.Printf("%10s %v\n", "Bool:",       gconv.Bool(i))
    fmt.Printf("%10s %v\n", "String:",     gconv.String(i))

    fmt.Printf("%10s %v\n", "Bytes:",      gconv.Bytes(i))
    fmt.Printf("%10s %v\n", "Strings:",    gconv.Strings(i))
    fmt.Printf("%10s %v\n", "Ints:",       gconv.Ints(i))
    fmt.Printf("%10s %v\n", "Floats:",     gconv.Floats(i))
    fmt.Printf("%10s %v\n", "Interfaces:", gconv.Interfaces(i))
}

Map转换

package main

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

func main() {
	type User struct {
		Uid  int    `c:"uid"`
		Name string `c:"name"`
	}
	// 对象
	g.Dump(gconv.Map(User{
		Uid:  1,
		Name: "john",
	}))
	// 对象指针
	g.Dump(gconv.Map(&User{
		Uid:  1,
		Name: "john",
	}))

	// 任意map类型
	g.Dump(gconv.Map(map[int]int{
		100: 10000,
	}))
}

属性标签

package main

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

func main() {
	type User struct {
		Uid      int
		Name     string `c:"-"`
		NickName string `c:"nickname, omitempty"`
		Pass1    string `c:"password1"`
		Pass2    string `c:"password2"`
	}
	user := User{
		Uid:   100,
		Name:  "john",
		Pass1: "123",
		Pass2: "456",
	}
	g.Dump(gconv.Map(user))
}

自定义标签

package main

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

func main() {
	type User struct {
		Id   int    `c:"uid"`
		Name string `my-tag:"nick-name" c:"name"`
	}
	user := &User{
		Id:   1,
		Name: "john",
	}
	g.Dump(gconv.Map(user, "my-tag"))
}
Struct转换

自动创建对象

package main

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

func main() {
	type User struct {
		Uid  int
		Name string
	}
	params := g.Map{
		"uid":  1,
		"name": "john",
	}
	var user *User
	if err := gconv.Struct(params, &user); err != nil {
		panic(err)
	}
	g.Dump(user)
}

数据校验

数据校验-校验规则

内置了40种常用的校验规则

import “github.com/gogf/gf/util/gvalid”

required             格式:required                              说明:必需参数
required-if          格式:required-if:field,value,...           说明:必需参数(当任意所给定字段值与所给值相等时,即:当field字段的值为value时,当前验证字段为必须参数)
required-unless      格式:required-unless:field,value,...       说明:必需参数(当所给定字段值与所给值都不相等时,即:当field字段的值不为value时,当前验证字段为必须参数)
required-with        格式:required-with:field1,field2,...       说明:必需参数(当所给定任意字段值不为空时)
required-with-all    格式:required-with-all:field1,field2,...   说明:必须参数(当所给定所有字段值都不为空时)
required-without     格式:required-without:field1,field2,...    说明:必需参数(当所给定任意字段值为空时)
required-without-all 格式:required-without-all:field1,field2,...说明:必须参数(当所给定所有字段值都为空时)
date                 格式:date                                  说明:参数为常用日期类型,格式:2006-01-02, 20060102, 2006.01.02
date-format          格式:date-format:format                    说明:判断日期是否为指定的日期格式,format为Go日期格式(可以包含时间)
email                格式:email                                 说明:EMAIL邮箱地址
phone                格式:phone                                 说明:手机号
telephone            格式:telephone                             说明:国内座机电话号码,"XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX"
passport             格式:passport                              说明:通用帐号规则(字母开头,只能包含字母、数字和下划线,长度在6~18之间)
password             格式:password                              说明:通用密码(任意可见字符,长度在6~18之间)
password2            格式:password2                             说明:中等强度密码(在弱密码的基础上,必须包含大小写字母和数字)
password3            格式:password3                             说明:强等强度密码(在弱密码的基础上,必须包含大小写字母、数字和特殊字符)
postcode             格式:postcode                              说明:中国邮政编码
resident-id          格式:resident-id                           说明:公民身份证号码
bank-card            格式:bank-card                             说明:银行号验证
qq                   格式:qq                                    说明:腾讯QQ号码
ip                   格式:ip                                    说明:IPv4/IPv6地址
ipv4                 格式:ipv4                                  说明:IPv4地址
ipv6                 格式:ipv6                                  说明:IPv6地址
mac                  格式:mac                                   说明:MAC地址
url                  格式:url                                   说明:URL
domain               格式:domain                                说明:域名
length               格式:length:min,max                        说明:参数长度为min到max(长度参数为整形),注意中文一个汉字占3字节
min-length           格式:min-length:min                        说明:参数长度最小为min(长度参数为整形),注意中文一个汉字占3字节
max-length           格式:max-length:max                        说明:参数长度最大为max(长度参数为整形),注意中文一个汉字占3字节
between              格式:between:min,max                       说明:参数大小为min到max(支持整形和浮点类型参数)
min                  格式:min:min                               说明:参数最小为min(支持整形和浮点类型参数)
max                  格式:max:max                               说明:参数最大为max(支持整形和浮点类型参数)
json                 格式:json                                  说明:判断数据格式为JSON
integer              格式:integer                               说明:整数
float                格式:float                                 说明:浮点数
boolean              格式:boolean                               说明:布尔值(1,true,on,yes:true | 0,false,off,no,"":false)
same                 格式:same:field                            说明:参数值必需与field参数的值相同
different            格式:different:field                       说明:参数值不能与field参数的值相同
in                   格式:in:value1,value2,...                  说明:参数值应该在value1,value2,...中(字符串匹配)
not-in               格式:not-in:value1,value2,...              说明:参数值不应该在value1,value2,...中(字符串匹配)
regex                格式:regex:pattern                         说明:参数值应当满足正则匹配规则pattern
数据校验-单数据校验

示例1,校验数据长度,使用默认的错误提示

rule := "length:6,16"
if e := gvalid.Check("123456", rule, nil);  e != nil {
    fmt.Println(e.String())
}
if e := gvalid.Check("12345", rule, nil);  e != nil {
    fmt.Println(e.String())
}

// 输出: 字段长度为6到16个字符

示例2,校验数据类型及大小,并且使用自定义的错误提示

rule := "integer|between:6,16"
msgs := "请输入一个整数|参数大小不对啊老铁"
if e := gvalid.Check(5.66, rule, msgs); e != nil {
    fmt.Println(e.Map())
}

// 输出: map[integer:请输入一个整数 between:参数大小不对啊老铁]
rule := "url|min-length:11"
msgs := map[string]string{
    "url"        : "请输入正确的URL地址",
    "min-length" : "地址长度至少为:min位"
}
if e := gvalid.Check("https://goframeorg", rule, msgs); e != nil {
    fmt.Println(e.Map())
}

// 输出: map[url:请输入正确的URL地址]

示例3,使用自定义正则校验数据格式,使用默认错误提示

// 参数长度至少为6个数字或者6个字母,但是总长度不能超过16个字符
rule := `regex:\d{6,}|\D{6,}|max-length:16`
if e := gvalid.Check("123456", rule, nil);  e != nil {
    fmt.Println(e.Map())
}
if e := gvalid.Check("abcde6", rule, nil); e != nil {
    fmt.Println(e.Map())
}

// 输出: map[regex:字段值不合法]
数据校验-多数据校验

示例1,默认错误提示

params := map[string]interface{} {
    "passport"  : "",
    "password"  : "123456",
    "password2" : "1234567",
}
rules := map[string]string {
    "passport"  : "required|length:6,16",
    "password"  : "required|length:6,16|same:password2",
    "password2" : "required|length:6,16",
}
if e := gvalid.CheckMap(params, rules); e != nil {
    fmt.Println(e.Maps())
}

// 输出: map[passport:map[required:字段不能为空 length:字段长度为6到16个字符] password:map[same:字段值不合法]]

示例2,自定义错误提示

params := map[string]interface{} {
    "passport"  : "",
    "password"  : "123456",
    "password2" : "1234567",
}
rules := map[string]string {
    "passport"  : "required|length:6,16",
    "password"  : "required|length:6,16|same:password2",
    "password2" : "required|length:6,16",
}
msgs  := map[string]interface{} {
    "passport" : "账号不能为空|账号长度应当在:min到:max之间",
    "password" : map[string]string {
        "required" : "密码不能为空",
        "same"     : "两次密码输入不相等",
    },
}
if e := gvalid.CheckMap(params, rules, msgs); e != nil {
    fmt.Println(e.String())
}

// 输出:账号不能为空; 账号长度应当在6到16之间; 两次密码输入不相等

缓存管理

import “github.com/gogf/gf/os/gcache”

缓存管理-内存缓存

示例1,基本使用
package main

import (
    "fmt"
    "github.com/gogf/gf/os/gcache"
)

func main() {
    // 创建一个缓存对象,
    // 当然也可以便捷地直接使用gcache包方法
    c := gcache.New()

    // 设置缓存,不过期
    c.Set("k1", "v1", 0)

    // 获取缓存
    v, _ := c.Get("k1")
    fmt.Println(v)

    // 获取缓存大小
    n, _ := c.Size()
    fmt.Println(n)

    // 缓存中是否存在指定键名
    b, _ := c.Contains("k1")
    fmt.Println(b)

    // 删除并返回被删除的键值
    fmt.Println(c.Remove("k1"))

    // 关闭缓存对象,让GC回收资源
    c.Close()
}
示例2,缓存控制
package main

import (
    "fmt"
    "github.com/gogf/gf/os/gcache"
    "time"
)

func main() {
    // 当键名不存在时写入,设置过期时间1000毫秒
    gcache.SetIfNotExist("k1", "v1", 1000*time.Millisecond)

    // 打印当前的键名列表
    keys, _ := gcache.Keys()
    fmt.Println(keys)

    // 打印当前的键值列表
    values, _ := gcache.Values()
    fmt.Println(values)

    // 获取指定键值,如果不存在时写入,并返回键值
    v, _ := gcache.GetOrSet("k2", "v2", 0)
    fmt.Println(v)

    // 打印当前的键值对
    data1, _ := gcache.Data()
    fmt.Println(data1)

    // 等待1秒,以便k1:v1自动过期
    time.Sleep(time.Second)

    // 再次打印当前的键值对,发现k1:v1已经过期,只剩下k2:v2
    data2, _ := gcache.Data()
    fmt.Println(data2)
}
示例3,GetOrSetFunc*
// 根据关键字进行markdown文档搜索,返回文档path列表
func SearchMdByKey(key string) ([]string, error) {
	v, err := gcache.GetOrSetFunc("doc_search_result_"+key, func() (interface{}, error) {
		// 当该key的检索缓存不存在时,执行检索
		array := garray.NewStrArray()
		docPath := g.Cfg().GetString("doc.path")
		paths, err := gcache.GetOrSetFunc("doc_files_recursive", func() (interface{}, error) {
			// 当目录列表不存在时,执行检索
			return gfile.ScanDir(docPath, "*.md", true)
		}, 0)
		if err != nil {
			return nil, err
		}
		// 遍历markdown文件列表,执行字符串搜索
		for _, path := range gconv.Strings(paths) {
			content := gfile.GetContents(path)
			if len(content) > 0 {
				if strings.Index(content, key) != -1 {
					index := gstr.Replace(path, ".md", "")
					index = gstr.Replace(index, docPath, "")
					array.Append(index)
				}
			}
		}
		return array.Slice(), nil
	}, 0)
	if err != nil {
		return nil, err
	}
	return gconv.Strings(v), nil
}
示例4,LRU缓存淘汰控制
package main

import (
    "github.com/gogf/gf/os/gcache"
    "time"
    "fmt"
)

func main() {
    // 设置LRU淘汰数量
    c := gcache.New(2)

    // 添加10个元素,不过期
    for i := 0; i < 10; i++ {
        c.Set(i, i, 0)
    }
    n, _ := c.Size()
    fmt.Println(n)
    keys, _ := c.Keys()
    fmt.Println(keys)

    // 读取键名1,保证该键名是优先保留
    v, _ := c.Get(1)
    fmt.Println(v)

    // 等待一定时间后(默认1秒检查一次),
    // 元素会被按照从旧到新的顺序进行淘汰
    time.Sleep(2*time.Second)
    n, _ = c.Size()
    fmt.Println(n)
    keys, _ = c.Keys()
    fmt.Println(keys)
}

模板引擎

示例1,解析模板文件

index.tpl

id:{{.id}}, name:{{.name}}

main.go

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/template", func(r *ghttp.Request) {
		r.Response.WriteTpl("index.tpl", g.Map{
			"id":   123,
			"name": "john",
		})
	})
	s.SetPort(8199)
	s.Run()
}

示例2,解析模板内容

package main

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

func main() {
	s := g.Server()
	s.BindHandler("/template", func(r *ghttp.Request){
		tplContent := `id:{{.id}}, name:{{.name}}`
		r.Response.WriteTplContent(tplContent, g.Map{
			"id"   : 123,
			"name" : "john",
		})
	})
	s.SetPort(8199)
	s.Run()
}

示例3,自定义模板分隔符

// main.go
package main

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

func main() {
    v := g.View()
    v.SetDelimiters("${", "}")   //这里定义新的
    b, err := v.Parse("gview_delimiters.tpl", map[string]interface{} {
        "k" : "v",
    })
    fmt.Println(err)
    fmt.Println(string(b))
}

修改模板目录

  1. 单例模式获取全局View对象,通过SetPath方法手动修改
  2. 修改命令行启动参数 - gf.gview.path
  3. 修改指定的环境变量 - GF_GVIEW_PATH
模板引擎-模板配置
[viewer]
    Paths       = ["/var/www/template"] # 模板文件搜索目录路径,建议使用绝对路径。默认为当前程序工作路径
    DefaultFile = "index.html"          # 默认解析的模板引擎文件。默认为"index.html"
    Delimiters  =  ["${", "}"]          # 模板引擎变量分隔符号。默认为 ["{{", "}}"]
    AutoEncode  = false                 # 是否默认对变量内容进行XSS编码。默认为false
    [viewer.Data]                       # 自定义的全局Key-Value键值对,将在模板解析中可被直接使用到
        Key1 = "Value1"
        Key2 = "Value2"

随后可以使用g.View()获取默认的单例对象时自动获取并设置该配置。

多个配置项

[viewer]
    paths       = ["template", "/var/www/template"]
    defaultFile = "index.html"
    delimiters  =  ["${", "}"]
    [viewer.data]
        name    = "gf"
        version = "1.10.0"
    [viewer.view1]
        defaultFile = "layout.html"
        delimiters  = ["${", "}"]
    [viewer.view2]
        defaultFile = "main.html"
        delimiters  = ["#{", "}"]

我们可以通过单例对象名称获取对应配置的View单例对象:

// 对应 viewer.view1 配置项
v1 := g.View("view1")
// 对应 viewer.view2 配置项
v2 := g.View("view2")
// 对应默认配置项 viewer
v3 := g.View("none")
// 对应默认配置项 viewer
v4 := g.View()
模板引擎-模板标签

使用 . 来访问当前对象的值(模板局部变量)。

使用 $ 来引用当前模板根级的上下文(模板全局变量)。

使用 $var 来访问特定的模板变量。

模板中支持的 go 语言符号

{{"string"}}     // 一般 string
{{`raw string`}} // 原始 string
{{'c'}}          // byte
{{print nil}}    // nil 也被支持

模板中的 pipeline

{{. | FuncA | FuncB | FuncC}}

if … else … end

range … end

with … end

define 自定义模板内容块(给一段模板内容定义一个模板名称),可用于模块定义和模板嵌套

template

include 在模板中可以使用include标签载入其他模板

与template标签的区别是:include仅支持文件路径,不支持模板名称;而tempalte标签仅支持模板名称,不支持文件路径。

模板引擎-模板函数

and and会逐一判断每个参数,将返回第一个为空的参数,否则就返回最后一个非空参数

{{and .X .Y .Z}}

call call可以调用函数,并传入参数

{{call .Field.Func .Arg1 .Arg2}}

index index支持map, slice, array, string,读取指定类型对应下标的值。

len 返回对应类型的长度

not 返回输入参数的否定值

or

print

printf

println

urlquery 字符串网址化

eq / ne / lt / le / gt / ge

text 将value变量值去掉HTML标签,仅显示文字内容(并且去掉script标签)

{{.value | text}}

htmlencode/encode/html 将value变量值进行html转义

htmldecode/decode 将value变量值进行html反转义

urlencode/url 将url变量值进行url转义

urldecode 将url变量值进行url反转义

date 将timestamp时间戳变量进行时间日期格式化

{{.timestamp | date .format}}
{{date .format .timestamp}}
{{date .format}}

compare 将str1和str2进行字符串比较

replace 将str中的search替换为replace

substr 将str从start索引位置(索引从0开始)进行字符串截取length,支持中文

strlimit 将str字符串截取length长度,支持中文

concat 拼接字符串

hidestr 将str字符串按照percent百分比从字符串中间向两边隐藏字符

{{"热爱GF热爱生活" | hidestr 20  "*"}}

highlight 将str字符串中的关键字key按照定义的颜色color进行前置颜色高亮

toupper/tolower 将str字符串进行大小写转换。

nl2br 将str字符串中的\n/\r替换为html中的标签

dump 格式化打印变量,功能类似于g.Dump方法

map 将模板变量转换为map[string]interface{}类型,常用于range…end遍历。

maps 将模板变量转换为[]map[string]interface{}类型,常用于range…end遍历。

json 将模板变量转换为JSON字符串,常用于将模板变量内容嵌入到Javacript代码中。

模板函数-自定义函数
package main

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

// 用于测试的带参数的内置函数
func funcHello(name string) string {
	return fmt.Sprintf(`Hello %s`, name)
}

func main() {
	// 绑定全局的模板函数
	g.View().BindFunc("hello", funcHello)

	// 普通方式传参
	parsed1, err := g.View().ParseContent(`{{hello "GoFrame"}}`, nil)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(parsed1))

	// 通过管道传参
	parsed2, err := g.View().ParseContent(`{{"GoFrame" | hello}}`, nil)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(parsed2))
}
模板引擎-模板变量

变量对象

我们可以在模板中使用自定义的对象,并可在模板中访问对象的属性及调用其方法。

package main

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

type T struct {
    Name string
}

func (t *T) Hello(name string) string {
    return "Hello " + name
}

func (t *T) Test() string {
    return "This is test"
}

func main() {
    t := &T{"John"}
    v := g.View()
    content := `{{.t.Hello "there"}}, my name's {{.t.Name}}. {{.t.Test}}.`
    if r, err := v.ParseContent(content, g.Map{"t" : t}); err != nil {
        g.Dump(err)
    } else {
        g.Dump(r)
    }
}
模板引擎-模板布局

gview模板引擎支持两种layout模板布局方式:

define + template方式
include模板嵌入方式

define + template方式

layout.html

 <!DOCTYPE html>
 <html>
 <head>
     <title>GoFrame Layout</title>
     {{template "header" .}}
 </head>
 <body>
     <div class="container">
     {{template "container" .}}
     </div>
     <div class="footer">
     {{template "footer" .}}
     </div>
 </body>
 </html>

header.html

 {{define "header"}}
     <h1>{{.header}}</h1>
 {{end}}

container.html

  {{define "container"}}
 <h1>{{.container}}</h1>
 {{end}}

footer.html

 {{define "footer"}}
 <h1>{{.footer}}</h1>
 {{end}}

include模板嵌入方式

layout.html

 {{include "header.html" .}}
 {{include .mainTpl .}}
 {{include "footer.html" .}}

header.html

 <h1>HEADER</h1>

footer.html

 <h1>FOOTER</h1>

main1.html

<h1>MAIN1</h1>

main2.html

<h1>MAIN2</h1>
模板引擎-XSS处理

配置文件

[viewer] autoencode = true

模板引擎-I18N变量
package main

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

func main() {
	content := `{{.name}} says "a{#hello}{#world}!"`
	result1, _ := g.View().ParseContent(content, g.Map{
		"name":         "john",
		"I18nLanguage": "zh-CN",
	})
	fmt.Println(result1)

	result2, _ := g.View().ParseContent(content, g.Map{
		"name":         "john",
		"I18nLanguage": "ja",
	})
	fmt.Println(result2)
}