- 背景
- 安装使用
- 非常简单的例子
- Restful API 的实现
- Get/Post
- 配置路由
- 配置路由参数
- 配置分组路由
- 接收 Query 参数
- [传入]Query/DefaultQuery 接收单个参数
- [传入]QueryArray 接收多个参数
- [传入]QueryMap 接收多个参数
- [传入]请求提 query 转 struct
- 接收表单数据
- [传入]PostForm 接收表单数据
- [传入]请求体 form 表单转 struct
- 文件上传
- [传入]单文件上传
- [传入]多文件上传
- JSON 的使用
- [输出]struct 转 json
- [输出]自定义 json 字段名
- [输出]json 数组
- [输出]json 格式美化
- [输出]json 输出不自动转义
- [输出]中文字符转成 unicode
- [传入]请求体 json 转 struct
- req/resp结构体中的tag
- 综合使用
- form/json/query
- required:必填
- omitempty:非必填
- neither requierd nor omitempty:
- binding
- gorm的一些tag
- thrift的一些tag
- 跨域问题
- JSONP
- CORS
- 代理
- XML 的使用
- [输出]返回 map
- [输出]struct 转 xml
- [输出]自定义 xml 字段命名
- [输出]xml 数组
- HTML 渲染
- html/template
- 模版界定符
- LoadHTMLFiles
- LoadHTMLGlob
- 自定义函数
- 托管文件
- StaticFile & Static
- 实现一个 FTP Server
- 自定义托管类型
- 重定向
- Redirect 重定向
- 同步和异步
- 同步
- 异步
- 中间件
- Logger & Recovery
- HTTP Basic Authorization
- 自定义全局中间件
- 自定义单个路由中间件
- 结合 MySQL
- 原生 database/sql
- gorm 工具
- 结合 Redis
- Thrift 的使用
- 项目实战
- 其他
golang 语言学完了,然后了解一下经典网站建站的 mvc 方式之后,我们就可以开始从 gin 框架官网 doc 文档入手,开始了解学习 gin 框架,官网文档入口:https://gin-gonic.com/zh-cn/docs/
我们对比下主流的一些框架,gin 是性能很优秀的框架,当然还有其他优秀的框架,下面筛出了比较优秀的几个框架
| 框架 | 在一定的时间内实现的总调用数 [越高越好] | 单次操作耗时(ns/op) [越低越好] | 堆内存分配 (B/op) [越低越好] | 每次操作的平均内存分配次数(allocs/op) [越低越好] |
|---|---|---|---|---|
| gin | 43550 | 27364 ns/op | 0 B/op | 0 allocs/op |
| Aero | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op |
| HttpRoute | 55938 | 21360 ns/op | 0 B/op | 0 allocs/op |
| LARS | 47779 | 25084 ns/op | 0 B/op | 0 allocs/op |
| Beego | 7407 | 243496 ns/op | 71456 B/op | 609 allocs/op |
| Martini | 331 | 3444706 ns/op | 226551 B/op | 2325 allocs/op |
默认你已经有了如下基础:
- 学些了 golang 语言
- 安装了 go 并且配置了 GOPATH 和 module
- 了解了 web 的 mvc 模式
- 已经安装了 go 的 ide 工具
安装 gin 到 $GOPATH/pkg 中
go get -u github.com/gin-gonic/gin
gin 引入代码中
import "github.com/gin-gonic/gin"
(可选)如果使用诸如 http.StatusOK 之类的常量,则需要引入 net/http 包
import "net/http"非常简单的例子
我们创建好一个 project 之后,我们再创建一个 main.go 文件
这里 gin.Default() 创建一个路由 Handler,路由 Handler 可以调用 GET/POST 等多种方法(做了封装),并且 gin.Context 中封装了 request 和 response 信息,最后使用 Run() 函数来监听端口
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
然后我们运行起来这个服务,打开我们的浏览器http://localhost:8080/ping就能正常展示返回的 json 信息了
我们看下 server 控制台的信息
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /ping --> main.main.func1 (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080Restful API 的实现 Get/Post
最常见的请求方式有 Get 和 Post,实现样例代码如下,这里指给出样例但是会忽略具体业务逻辑部分
type User struct {
ID uint64
Name string
}
func main() {
// 从数据库中取到用户信息
users := []User{{ID: 123, Name: "张三"}, {ID: 456, Name: "李四"}}
// 获取一个 gin 实例
r := gin.Default()
// get 请求
r.GET("/users", func(c *gin.Context) {
c.JSON(200, users)
})
// 服务启动
r.Run(":8080")
}
配置路由
配置路由参数
gin 路由采用 httprouter 我们先看下面一个例子
func main() {
// 创建 gin 实例
r := gin.Default()
// get 请求
r.GET("/users/:id", func(c *gin.Context) {
// 拿到路由中的 id 值
id := c.Param("id")
// 响应值
c.String(200, "The user id is %s", id)
})
// 运行服务
r.Run(":8080")
}
我们浏览器中输入localhost:8080/users/222或者输入localhost:8080/users/呃呃呃都是可以匹配的。还有要注意相同模式的路由不能重复注册
/users/:id :id 的路由可以匹配数字或字符串 /users/*id *id 表示后面有几层路径或者没有路径,比如说我路径是 /user/333 那么其实 id 就是 “/333”,不建议使用,因为能匹配到的太多了配置分组路由
假如我们要升级版本 API,但是旧版本我们也要保留来兼容老用户,我们可以通过 Group 方法这么做
func main() {
// 一个 gin 实例
r := gin.Default()
// V1 版本 API
v1Group := r.Group("/v1")
v1Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v1/users")
})
v1Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v1/products")
})
// V2 版本 API
v2Group := r.Group("/v2")
v2Group.GET("/users", func(c *gin.Context) {
c.String(200, "/v2/users")
})
v2Group.GET("/products", func(c *gin.Context) {
c.String(200, "/v2/products")
})
// 运行服务
r.Run(":8080")
}
然后我们可以细看一下这个 Group 函数,第一个参数就是分组路由命名空间,第二个参数是分组路由的中间件,即分组路由下的子路由在执行请求的时候会调用这个中间件方法
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
那么这个中间件具体有什么好处呢,比如说某分组路由下面都需要经过校验处理:
v1Group := r.Group("/admin", func(c *gin.Context) {
fmt.Println("/admin分组路由中间件,在请求/admin的子路由时候将会打印出来")
})
接收 Query 参数
[传入]Query/DefaultQuery 接收单个参数
我们看一个例子
func main() {
// 一个 gin 实例
r := gin.Default()
// get 请求
r.GET("/user", func(c *gin.Context) {
c.String(200, c.DefaultQuery("id", "0"))
})
// 运行服务
r.Run(":8080")
}
然后我们在浏览器输入localhost:8080/user?id=1,上面使用了 DeafaultQuery 方法在请求时候如果去不到就用默认值 0 来代替,如果是直接字符串的输入我们可以用 Query 方法即可
c.Query("name")
// 返回两个参数,底层是:c.Request.URL.Query().Get(key)
v, ok := c.GetQuery("name")
[传入]QueryArray 接收多个参数
// http://localhost:8080/?name=xiaoming&name=zhanghua
c.QueryArray("name")
[传入]QueryMap 接收多个参数
不常用
// http://localhost:8080/map?name[a]=liming&name[b]=zhanghua&name[c]=xiaohua
c.QueryMap("name")
[传入]请求提 query 转 struct
使用 BindQuery 或者是 ShouldBindQuery 函数
var usr User
// 这一步实现接收的 query 绑定到 stuct 实例中
if c.ShouldBindQuery(&usr) != nil {
// ...
}
接收表单数据
[传入]PostForm 接收表单数据
func main() {
// 获取 gin 实例
r := gin.Default()
// post 请求
r.POST("/", func(c *gin.Context) {
// PostForm 接收表单数据 name
usr := c.PostForm("usr")
c.String(200, usr)
})
// 运行服务
r.Run(":8080")
}
[传入]请求体 form 表单转 struct
使用 Bind 函数来将表单信息绑定在一个结构体实例上
type User struct {
Name string `form: "name"`
Age int `form: "age"`
}
func main() {
// 创建一个 gin 实例
r := gin.Default()
// post 请求
r.Post("/form", func(c *gin.Context) {
// 一个表单实例
var usr User
// Bind 函数绑定表单,并进行错误处理
if err := c.Bind(&usr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断表单
if usr.Name != "zhanghua" || usr.Age != 12 {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
// 最后正常返回
c.JSON(http.StatusOK, gin.H{"status": "you are right"})
})
}
一般数据绑定是自动根据请求的 content-type 来确定类型的,如果你明确知道数据类型到底是传入的 json 还是传入的 form 形式,我们也可以用 BindWith 函数
c.BindWith(&form, binding.Form)文件上传 [传入]单文件上传 [传入]多文件上传 JSON 的使用
gin 框架中提供了两种 json 解析器,一种默认的使用 golang 语言中的内置 json,另一种使用 jsoniter,如果要使用 jsoniter
go build -tags=jsoniter .[输出]struct 转 json
这样输出的时候一个结构体也是以 json 的形式来输出了
type User struct {
Name string
age int
}
func main() {
// 一个 gin 实例
r := gin.Default()
// 一个 get 请求
r.Get("/users/zhanghua", func(c *gin.Context) {
c.Json(200, User{Name: "zhanghua", Age: 12})
})
// 运行服务
r.Run(":8080")
}
输出结果如下:
{"Name": "zhanghua", "Age": 12}
[输出]自定义 json 字段名
我们可以看到上面的例子,发现最后网站上输出的都是大写的参数名,但是我们平常看到的 json 参数名其实都是小写的,所以我们需要把 struct 中的参数名转成小写的,gin 是支持字段名字重新命名的,通过在 tag 中重写名字即可
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
[输出]json 数组
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// 一个 gin 实例
r := gin.Default()
// 用户数组
users := []user{
{Name: "zhanghua", Age: 12},
{Name: "liming", Age: 23}
}
// get 请求
r.Get("/users", func(c *gin.Context) {
c.IntentedJSON(200, users)
})
}
最后输出
[
{
"name": "zhanghua",
"age": 12
},
{
"name": "liming",
"age": 23
}
]
[输出]json 格式美化
// 没有缩紧美化效果 c.JSON() // 有缩紧美化效果 c.IntentedJSON()[输出]json 输出不自动转义
gin 框架对于输出 json,对于 json 中的值是可以自动转义的,但是如果使用 PureJSON() 方法就可以不自动转义了
func main() {
// 创建一个 gin 实例
r := gin.Default()
// 一个 get 请求
r.Get("/pureJson", func(c *gin.Context) {
c.PureJSON(200, gin.H{
"name": "zhanghua"
})
})
}
因为我是用了 PureJson() 函数,所以最后输出依然含有,而不是被自动转义成了xxxx的形式
[输出]中文字符转成 unicode如果要把非 Ascll 字符转成 unicode 编码,避免后续乱码,比如说把中文字符最后输出都转成xxxx形式的 unicode 编码,gin 中可以使用 AsciiJSON() 函数来实现
[传入]请求体 json 转 struct也称为 json 绑定,实际就是把请求体中的 json 数据按照 json 格式进行解析,解析后存储到 struct 中,json 反序列化方法有 BindJSON 和 ShouldBindJSON
type User struct {
Name string `form: "name" json: "name"`
Age int `form: "age" json: "age"`
}
func main() {
// 创建一个 gin 实例
r := gin.Default()
// post 请求
r.POST("/bindjson", func(c *gin.Context) {
// 声明一个实例
var usr User
// 把请求体 json 转成一个实例 struct,并进行错误判断
if err := c.ShouldBindJSON(&usr); err != null {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断是否匹配
if usr.Name != "zhanghua" || usr.Age != 12 {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
// 最后匹配
c.JSON(http.StatusOK, gin.H{"status": "you are right"})
})
// 运行服务
r.Run(":8080")
}
实际请求是怎么做的呢,通过请求提传递 json 参数
{"name": "zhanghua", "age": 12}
req/resp结构体中的tag
综合使用
常见 req 和 resp 中会有,一般这里举的例子,如下用的最多
type Bar struct {
Name string `form:"name,required" json:"name,required" query:"name,required"`
Age int `form:"age" json:"age,omitempty" query:"age"`
}
form/json/query
这些 tag 非常常见,看上方例子,这里不赘述
required:必填必填写字段
omitempty:非必填如果omitempty标记的字段为"",那么最后接受者会发现没有omitempty标记的字段
neither requierd nor omitempty: binding例子:
Name string `form:"name" json:"name" binding:"required,min=3,max=10"`
后面代码用 ShouldBindJson 函数来承接
gorm的一些tag如下例子用的非常多
Id int64 `gorm:"primary_key;column:user_id;type:bigint(20);not null"thrift的一些tag 跨域问题
gin 对 JSONP 提供了很好的支持,但是并不推荐使用,更加推荐使用的是 CORS 和代理的方式
JSONP
下面为服务端代码,这样的话{"name": "zhanghua"}就可以作为 data 参数传递给前端 sayHello 函数了
func main() {
r := gin.Default()
r.GET("/jsonp", func(c *gin.Context) {
c.JSONP(200, gin.H{"name": "zhanghua"})
})
r.Run(":8080")
}
gin 是怎么避免


