Gin框架入门 | 青训营笔记

156 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记

1. Gin框架介绍

Gin官网:gin-gonic.com/

Github:github.com/gin-gonic/g…

官方中文文档:gin-gonic.com/zh-cn/docs/

2. Gin框架下载与安装

2.1. 安装Gin

在安装Gin之前,需要先安装Go并设置好Go的工作区:

  1. 下载并安装gin:go get -u github.com/gin-gonic/gin

注:go get命令依赖git,在使用go get获取前,请确保 GOPATH 已经设置且安装Git源码管理工具,Go 1.8 版本之后,GOPATH 默认在用户目录的 go 文件夹下。go get 下载保存目录为:$GOPATH/src

  1. 将gin集成到项目中

2.2. 快速入门

2.2.1. 使用net/http构建一个简单web服务

  1. 简单使用字符串进行响应
package main

import (
	"fmt"
	"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
	_, _ = fmt.Fprintf(w, "Hello Golang!")

}
func main() {
	http.HandleFunc("/hello", sayHello)
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
		fmt.Printf("http server failed, err:%v\n", err)
		return
	}

}

启动mian.go程序,使用浏览器访问localhost:9090/hello

  1. 读取文件进行响应
  • 在项目下新建一个hello.html文件

  • 补全代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
    <h1>Hello Golang!</h1>
</body>
</html>
  • 响应
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
	// 读取文件内容,会转为为字节数组返回
	byte, _ := ioutil.ReadFile("./hello.html")
	// 将字节数组强转为字符串进行响应
	_, _ = fmt.Fprintf(w, string(byte))

}
func main() {
	http.HandleFunc("/hello", sayHello)
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
		fmt.Printf("http server failed, err:%v\n", err)
		return
	}

}

重新启动mian.go程序,使用浏览器访问localhost:9090/hello

2.2.2. 第一个Gin应用

 package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()
	// GET:请求方式,/hello:请求路径
	// 当客户端以get方式请求/hello路径时,会执行后面的匿名函数
	r.GET("/hello", func(c *gin.Context){
		// c.JSON:返回JSON格式的数据
		c.JSON(200,gin.H{
			"message": "Hello Golang!",
		})
	})
	// 启动http服务,默认在0.0.0.0:8080启动服务
	r.Run()
    // 指定9090端口启动
    // r.Run(":9090")
    
}

2.2.3. 热加载配置

所谓热加载就是当我们对代码进行修改时,程序能够自动重新加载并执行,这在我们开发中是非常便利的,可以快速进行代码测试,省去了每次手动重新编译。beego中我们可以使用官方给我们提供的bee工具来热加载项目,但是gin中并没有官方提供的热加载工具,这个时候我们要实现热加载就可以借助第三方的工具。

go get github.com/pilu/fresh 
D:\gin_demo>fresh
go get -u github.com/codegangsta/gin 
D:\gin_demo>gin run main.go

3. Go web开发基础与工具

3.1. 网络基础

3.2. Go语言标准库之net/http

Golang构建HTTP服务(一)--- net/http库源码笔记

Golang net/http包详解

Golang net 包学习和实战

Go语言内置的net/http包提供了HTTP客户端和服务端的实现。

理解 HTTP 构建的网络应用只要关注两个端---客户端(clinet)和服务端(server),所谓的http服务器,主要在于如何接受客户端的request,并向客户端返回response

接收request的过程中,最重要的莫过于路由(router),即实现一个Multiplexer器。Go中既可以使用内置的Mutilplexer --- DefautServeMux,也可以自定义。Multiplexer路由的目的就是为了找到处理器函数(handler),后者将对request进行处理,同时构建response

简单总结就是这个流程为:

Clinet -> Requests -> Multiplexer(router) -> handler -> Response -> Clinet

3.3. Go语言标准库之template

www.liwenzhou.com/posts/Go/go…

www.jianshu.com/p/9091e8a34…

3.4. Restful风格API

REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源

只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。

例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:

请求方法URL含义
GET/book查询书籍信息
POST/create_book创建书籍记录
POST/update_book更新书籍信息
POST/delete_book删除书籍信息

同样的需求我们按照RESTful API设计如下:

请求方法URL含义
GET/book查询书籍信息
POST/book创建书籍记录
PUT/book更新书籍信息
DELETE/book删除书籍信息

Gin框架支持开发RESTful API的开发,示例如下:

func main() {
	r := gin.Default()
	r.GET("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "GET",
		})
	})

	r.POST("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "POST",
		})
	})

	r.PUT("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "PUT",
		})
	})

	r.DELETE("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "DELETE",
		})
	})
}

开发Restful API的时候我们通常使用Postman来作为客户端的测试工具,测试Restful API的功能是否正常。

3.5. Postman测试工具

使用Postman对上面的Restful API进行测试:

4. Gin基础

4.1. 路由概述

路由(Routing)是由一个 URI(或者叫路径)和一个特定的 HTTP 方法(GET、POST 等) 组成的,简单点说,就是当对服务器进行请求时,服务器需要使用哪个方法对其进行响应,而路由将请求路径及其对应的响应方法进行绑定。

Gin中的路由器不是使用Gonet/http包的默认mux,而是使用httprouter作为自己的路由器,httprouter有极高的性能,速度提高了近 40 倍。

4.2. 简单的路由配置

  1. 简单的路由配置
r := gin.Default()
r.GET("/book", func(c *gin.Context) { 
    c.JSON(200, gin.H{
        "message": "GET",
    })
})
  1. 路径参数传值

访问:域名/news?aid=20

r.GET("/news", func(c *gin.Context) { 
    aid := c.Query("aid") 
    c.String(200, "aid=%s", aid) 
})
  1. 动态路由

访问:域名/user/20

r.GET("/user/:uid", func(c *gin.Context) { 
    uid := c.Param("uid") 
    c.String(200, "userID=%s", uid) 
})

4.3. Gin响应数据类型

  • String类型
r.GET("/", func(c *gin.Context) { 
    c.String(200, "Hello World") 
})
  • JSON类型
// 方式一:自己拼接JSON
r.GET("/json1", func(context *gin.Context) {
    // gin.H 是 map[string]interface{}的缩写
    // type H map[string]interface{}
    context.JSON(http.StatusOK, gin.H{
        "message": "hello",
    })
})
// 方式二:使用结构体
r.GET("/json2", func(context *gin.Context) {
    var msg struct {
        Name    string `json:"name"`
        Message string `json:"message"`
        Age     int
    }
    msg.Name = "sundegan"
    msg.Message = "Hello World"
    msg.Age = 18
    context.JSON(http.StatusOK, msg)

})
  • JSONP类型
r.GET("/jsonp", func(context *gin.Context) {
    data := gin.H{
        "message": "hello",
    }
    context.JSONP(http.StatusOK, data)
})
  • XML类型
r.GET("/xml", func(context *gin.Context) {
    context.XML(http.StatusOK, gin.H{
        "message": "hello",
    })
})
  • HTML渲染模板

注:使用HTML渲染模板数据,渲染模板前需要使用 LoadHTMLGlob()或者 LoadHTMLFiles()方法加载模板。示例:r.LoadHTMLGlob("templates/*"),指定从templates目录下加载模板。

r := gin.Default()
r.LoadHTMLGlob("templates/*") // 配置模板路径

r.GET("/index", func(context *gin.Context) {
    context.HTML(http.StatusOK, "index.html", gin.H{
        "title": "首页",
    })
})

4.5. 运行多个服务

我们可以在多个端口启动服务,例如:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

var (
	g errgroup.Group
)

func router01() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"error": "Welcome server 01",
			},
		)
	})

	return e
}

func router02() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"error": "Welcome server 02",
			},
		)
	})

	return e
}

func main() {
	server01 := &http.Server{
		Addr:         ":8080",
		Handler:      router01(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	server02 := &http.Server{
		Addr:         ":8081",
		Handler:      router02(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
   // 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
	g.Go(func() error {
		return server01.ListenAndServe()
	})

	g.Go(func() error {
		return server02.ListenAndServe()
	})

	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}

5. Gin模板渲染

5.1. 模板加载配置

  1. 单模板文件目录(所有模板文件放在一个目录下)

func main() {
	r := gin.Default()
	r.LoadHTMLGlob("templates/*") // 单模板目录配置,去templates目录下匹配模板文件

	r.GET("/news", func(context *gin.Context) {
		type Article struct {
			Title   string
			Content string
		}
		news := &Article{
			Title:   "新闻标题",
			Content: "新闻内容",
		}
		context.HTML(http.StatusOK, "news.html", gin.H{
			"title": "新闻页面",
			"news":  news, // 传结构体数据
		})
	})
	r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
  1. 多模板文件目录()

当有多个模板文件目录时,且目录下有同名页面时,当加载页面时,程序就不知道该从哪里进行加载了,所以Gin 框架中如果不同目录下面有同名模板的话,需要注意以下两点:

  • r.LoadHTMLGlob("templates/**/*") // 多模板目录配置,去templates目录下的子目录中匹配模板文件
  • 使用define给模板文件定义名字,使用模板名称对模板进行调用

// define相当于给模板定义一个名字,define和end必须成对
{{ define "admin/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>admin index</title>
</head>
<body>
    <h1>后台模板</h1>
    <h1>{{.title}}</h1>
</body>
</html>
{{ end }}
{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>前台模板</h1>
    <h1>{{.title}}</h1>
</body>
</html>
{{ end }}
func main() {
	r := gin.Default()
	r.LoadHTMLGlob("templates/**/*") // 多模板目录配置,去templates目录下的子目录中匹配模板文件

	// 使用模板名字调用模板
	r.GET("/news", func(context *gin.Context) {
		context.HTML(http.StatusOK, "default/index.html", gin.H{
			"title": "前台首页",
		})
	})
	r.GET("/news", func(context *gin.Context) {
		context.HTML(http.StatusOK, "admin/index.html", gin.H{
			"title": "后台首页",
		})
	})
	r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

注意:如果模板在多级目录里面的话需要这样配置 r.LoadHTMLGlob("templates/**/**/*"),一个/** 表示一级目录。如果存在同名模板,需要使用define对模板进行命名,使用模板的名称对模板进行调用。

5.2. gin模板基本语法

  1. {{.}}

模板语法都包含在{{}}中间,其中{{.}}中的点表示gin.H这个对象,该对象实际是一个map。 可以使用多个.来访问map中的内容。例如访问map中结构体的对应字段:

func main() {
	r := gin.Default()
	r.LoadHTMLGlob("templates/**/*") // 多模板目录配置,去templates目录下的子目录中匹配模板文件

	// 使用模板名字调用模板
	r.GET("/", func(context *gin.Context) {
		type User struct {
			Name string
			Age int
		}
		user := User{
			Name: "张三",
			Age: 18,
		}
		context.HTML(http.StatusOK, "default/index.html", gin.H{
			"title": "前台首页",
			"user": user,
		})
	可以根据.来访问结构体的对应字段。例如:
	r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>前台模板</h1>
    <h1>{{.title}}</h1>

    <h2>{{.user.Name}}</h2>
    <h2>{{.user.Age}}</h2>
</body>
</html>
{{ end }}

注:map中的value可以为任意类型,如切片、结构体、map等,都可以通过.来对其进行取值。

  1. 注释

{{/* comment */}}

  1. 变量

还可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果。具体语法如下:

定义变量:{{$变量名 := .title}}

使用变量:{{$变量名}}

{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>前台模板</h1>
    <!-- 定义变量 -->
    {{$t := .title}}
    <!-- 使用变量 -->
    <h2>{{$t}}</h2>
</body>
</html>
{{ end }}
  1. 移除空格

有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲染出来的内容可能就和我们想的不一样,这个时候可以使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号。例如:{{- .Name -}}

<p>{{ 20 }} < {{ 40 }}---> 20 < 40</p>
<p>{{ 20 -}} < {{- 40 }}-->20<40</p>

注意:-要紧挨{{}},同时与模板值之间需要使用空格分隔。

  1. 比较函数

布尔函数会将任何类型的零值视为假,其余视为真。

eq  如果arg1 == arg2则返回真
ne  如果arg1 != arg2则返回真
lt  如果arg1 < arg2则返回真
le  如果arg1 <= arg2则返回真
gt  如果arg1 > arg2则返回真
ge  如果arg1 >= arg2则返回真
// 使用方式
<h1>比较函数</h1>
<p>{{gt 11 13}}</p>
<p>{{lt 11 13}}</p>
<p>{{eq 11 11}}</p>

为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:{{eq arg1 arg2 arg3}}

注:比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。但是,整数和浮点数不能互相比较。

  1. 条件判断

Go 模板语法中的条件判断有以下几种:

{{if pipeline}} T1 {{end}}

{{if pipeline}} T1 {{else}} T0 {{end}}

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

注:pipeline是指产生数据的操作,比如{{.}}{{.Name}}等。示例:

{{if gt .score > 20}}
    <h3>及格</h3>
{{else}}
    <h3>不及格</h3>
{{end}}
  1. range

Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道。

{{range $key,$value := .obj}} 
    {{$value}} 
{{end}}

如果 pipeline 的值其长度为 0,不会有任何输出。

{{$key,$value := .obj}} 
    {{$value}} 
{{else}} 
    TO 
{{end}}

如果 pipeline 的值其长度为 0,则会执行T0语句。

router.GET("/", func(c *gin.Context) { 
    c.HTML(http.StatusOK, "default/index.html", gin.H{ 
        "hobby": []string{"吃饭", "睡觉", "写代码"}, 
    }) 
})
{{range $key,$value := .hobby}} 
<p>{{$key}}------{{$value}}</p> 
{{end}}
  1. with
{{with pipeline}} T1 {{end}}
如果pipeline为空不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline为空,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。

with主要用来解构结构体,简化需要使用多个.进行输出的操作,如在输出结构体属性时:

user := UserInfo{ 
    Name: "张三", 
    Gender: "男", 
    Age: 18, 
}
router.GET("/", func(c *gin.Context) { 
    c.HTML(http.StatusOK, "default/index.html", gin.H{ 
        "user": user, 
    }) 
})

不使用with输出数据:

<h4>{{.user.Name}}</h4> 
<h4>{{.user.Gender}}</h4> 
<h4>{{.user.Age}}</h4>

使用with解构结构体输出数据:

{{with .user}} 
    <h4>姓名:{{.Name}}</h4> 
    <h4>性别:{{.Gender}}</h4> 
    <h4>年龄:{{.Age}}</h4> 
{{end}}

简单理解:相当于 var .=.user.(dot)绑定了新的对象。

  1. Gin模板预定义函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。预定义的全局函数如下:

and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len
    返回它的参数的整数类型长度(字节数)
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回与其参数的文本表示形式等效的转义HTML。
    这个函数在html/template中不可用。
urlquery
    以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
    这个函数在html/template中不可用。
js
    返回与其参数的文本表示形式等效的转义JavaScript。
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
    其中Y是函数类型的字段或者字典的值,或者其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有12个返回值,如果有2个则后一个必须是error接口类型;
    如果有2个返回值的方法返回的errornil,模板执行会中断并返回给调用模板执行者该错误;
{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>前台模板</h1>
    <!-- 使用预定义函数返回title长度 -->
    {{len .title}}
    <!-- 返回hobby切片第二个索引的数据 -->
    {{index .hobby 2}}
</body>
</html>
{{ end }}
  1. 自定义模板函数
// 自定义一个夸人的模板函数
func kuaren(name string) (string, error) {
    return name + "真帅", nil
}
func main() {
	r := gin.Default()
    
    // 注册全局模板函数,注意顺序,注册模板函数需要在加载模板之前
    router.SetFuncMap(template.FuncMap{ 
        "kuaren": kuaren, 
    })
    // 加载模板
	r.LoadHTMLGlob("templates/**/*") 

	r.GET("/", func(context *gin.Context) {
		type User struct {
			Name string
			Age  int
		}
		user := User{
			Name: "张三",
			Age:  18,
		}
		context.HTML(http.StatusOK, "default/index.html", gin.H{
			"title": "前台首页",
			"user":  user,
		})
	})
	r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

我们可以在模板文件index.html中按照如下方式使用我们自定义的kua函数了:{{kuaren .user.Name}}

  1. 模板嵌套

我们可以在模板中嵌套其他的模板。这个模板可以是单独的文件,也可以是通过define定义的模板。举个例子: 在index.html模板中引用其他模板。

{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>嵌套template</h1>
    <hr>
    <!-- 引入外部模板 -->
    {{template "public/header.html" .}}
    <hr>
    <!-- 引入内部模板 -->
    {{template "ol.html" .}}
</body>
</html>
{{ end }}

{{ define "ol.html"}}
<ol>
    <li>吃饭</li>
    <li>睡觉</li>
    <li>打豆豆</li>
</ol>
{{end}}

header.html

{{ define "public/header.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>admin index</title>
</head>
<body>
    <h1>公共头部标题</h1>
    <h1>我是一个公共标题---{{.title}}</h1>
</body>
</html>
{{ end }}
func main() {
	r := gin.Default()
    // 加载模板
	r.LoadHTMLGlob("templates/**/*") 

	r.GET("/", func(context *gin.Context) {
		context.HTML(http.StatusOK, "default/index.html", gin.H{
			"title": "前台首页",
		})
	})
	r.Run() 
}

注意:

  • 引入模板时使用的名字为给模板定义的名字。
  • 引入的时候注意最后的.,这样才能把数据传入到被引入的模板中,在被引入模板可以直接使用.使用原模板的数据。
  1. 模板继承

通过block、define、template实现模板继承。

{{ block "name" pipeline }} T {{ end }}

block等价于define定义一个名为name的模板,并在"有需要"的地方执行这个模板,执行时将.设置为pipeline的值。

等价于:先 {{ define "name" }} T {{ end }} 再执行 {{ template "name" pipeline }}

  1. 修改默认标识符

Go标准库的模板引擎使用的花括号{{}}作为标识,而许多前端框架(如Vue和 AngularJS)也使用{{}}作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:router.Delims("[[","]]")

5.3. text/template与html/tempalte的区别

html/template针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击。例如,我定义下面的模板文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    {{.}}
</body>
</html>

这个时候传入一段JS代码并使用html/template去渲染该文件,会在页面上显示出转义后的JS内容。 <script>alert('嘿嘿嘿')</script> 这就是html/template为我们做的事。使用html/template防御xss攻击示例如下:

// 1 定义函数
func safe (str string) template.HTML {
	return template.HTML(str)
}
//2 注册函数
	router.SetFuncMap(template.FuncMap{
		"parserTime": parserTime,
		"safe": safe,
	})

//3 模板中使用
<h1>xss攻击</h1>
<p>{{.str1}}</p>
<p>{{safe .str1 }}</p>

5.4. 静态文件处理

当我们渲染的HTML页面中引用了静态文件时, HTML页面其实会发起http请求去访问对应的静态资源,同样需要配置路由才能正确地对静态资源进行访问,所以需要配置静态web服务,配置静态资源路由使用Static()方法。如:r.Static("/static", "./static") 前面的/static表示路由,后面的./static表示路径。

func main() { 
    r := gin.Default() 
    r.Static("/static", "./static") // 第一个参数表示路由,第二个参数表示映射路径
    r.LoadHTMLGlob("templates/**/*") 
    // ... 
    r.Run(":8080") 
}
// html页面引用静态资源
<link rel="stylesheet" href="/static/css/base.css" />

6. Gin获取参数

6.1. 获取querystring参数

querystring指的是URL中?后面携带的参数,例如:/user/search?username=小王子&address=沙河。 获取请求的querystring参数的方法如下:

func main() {
	r := gin.Default()
	r.GET("/user/search", func(c *gin.Context) {
		username := c.DefaultQuery("username", "小王子")
		//username := c.Query("username")
		address := c.Query("address")
		//返回json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	r.Run()
}

6.2. 获取form参数

当前端请求的数据通过form表单提交时,例如向/user/search发送一个POST请求,获取请求数据的方式如下:

func main() {
	r := gin.Default()
	r.POST("/user/search", func(c *gin.Context) {
		// DefaultPostForm取不到值时会返回指定的默认值
		// username := c.DefaultPostForm("username", "小王子")
		username := c.PostForm("username")
		address := c.PostForm("address")
		// 返回json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	r.Run()
}

6.3. 获取json参数

当前端请求的数据通过JSON提交时,例如向/json发送一个POST请求,则获取请求参数的方式如下:

r.POST("/json", func(c *gin.Context) {
	// 注意:下面为了举例子方便,暂时忽略了错误处理
	b, _ := c.GetRawData()  // 从c.Request.Body读取请求数据
	// 定义map或结构体
	var m map[string]interface{}
	// 反序列化
	_ = json.Unmarshal(b, &m)

	c.JSON(http.StatusOK, m)
})

更便利的获取请求参数的方式,参见下面的 参数绑定 小节。

6.4. 获取路径参数

请求的参数通过URL路径传递,例如:/user/search/小王子/沙河。 获取请求URL路径中的参数的方式如下:

func main() {
	r := gin.Default()
	r.GET("/user/search/:username/:address", func(c *gin.Context) {
		username := c.Param("username")
		address := c.Param("address")
		//返回json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	r.Run()
}

6.5. 参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。

// Binding from JSON
type Login struct {
	User     string `form:"user" json:"user" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

func main() {
	router := gin.Default()

	// 绑定JSON的示例 ({"user": "q1mi", "password": "123456"})
	router.POST("/loginJSON", func(c *gin.Context) {
		var login Login

		if err := c.ShouldBind(&login); err == nil {
			fmt.Printf("login info:%#v\n", login)
			c.JSON(http.StatusOK, gin.H{
				"user":     login.User,
				"password": login.Password,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	// 绑定form表单示例 (user=q1mi&password=123456)
	router.POST("/loginForm", func(c *gin.Context) {
		var login Login
		// ShouldBind()会根据请求的Content-Type自行选择绑定器
		if err := c.ShouldBind(&login); err == nil {
			c.JSON(http.StatusOK, gin.H{
				"user":     login.User,
				"password": login.Password,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
	router.GET("/loginForm", func(c *gin.Context) {
		var login Login
		// ShouldBind()会根据请求的Content-Type自行选择绑定器
		if err := c.ShouldBind(&login); err == nil {
			c.JSON(http.StatusOK, gin.H{
				"user":     login.User,
				"password": login.Password,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	router.Run()
}

ShouldBind会按照下面的顺序解析请求中的数据完成绑定:

  1. 如果是 GET 请求,只使用 Form 绑定引擎(query)。
  2. 如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)。