gin基础知识点
# 使用gin的理由
- Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点
- 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的
net/http
足够简单,性能也非常不错 - 借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范
简单使用
安装
go get -u github.com/gin-gonic/gin
导入
import “github.com/gin-gonic/gin”
hello word示例
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main(){
// 1.创建路由
r := gin.Default()
// 2.绑定路由规则,执行的函数
// gin.Context,封装了request和response
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "hello World!")
})
// 3.监听端口,默认在8080
// Run("里面不指定端口号默认为8080")
// 指定端口避免:8080这个常用端口被占用
r.Run(":8000")
}
结果:
gin路由
基本路由
- gin 框架中采用的路由库是基于httprouter做的
- 因为虽然
net/http
这个包里有着默认路由,但是仍存在着不足,所以使用httprouter - httprouter 是一个高性能、可扩展的HTTP路由,上面我们列举的net/http默认路由的不足,都被httprouter 实现
- 要想了解更多的有关httprouter的知识,请访问:Git仓库地址
API
-
使用Restful风格的API(URL定位资源,用HTTP描述操作)
1.获取文章 /blog/getXxx Get blog/Xxx
2.添加 /blog/addXxx POST blog/Xxx
3.修改 /blog/updateXxx PUT blog/Xxx
4.删除 /blog/delXxxx DELETE blog/Xxx
参数获取
API参数
-
通过Context的Param方法来获取API参数
示例:
package main import ( "github.com/gin-gonic/gin" "net/http" "strings" ) func main() { r := gin.Default() r.GET("/user/:name/*action", func(c *gin.Context) { name := c.Param("name") action := c.Param("action") //c.String(http.StatusOK,"name = "+name+"action = "+action) //截取/ action = strings.Trim(action, "/") c.String(http.StatusOK, name+" is "+action) }) //默认为监听8080端口 r.Run(":8000") }
结果:
url参数
- URL参数可以通过DefaultQuery()或Query()方法获取
- DefaultQuery()若参数不村则,返回默认值,Query()若不存在,返回空串
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/user", func(c *gin.Context) {
//不指定默认值,若是不传参则接受的是空串
//name := c.Query("name")
//指定默认值
//http://localhost:8080/user 才会打印出来默认的值
name := c.DefaultQuery("name", "Re")
c.String(http.StatusOK, fmt.Sprintf("hello %s", name))
})
r.Run(":8000")
}
不传参
传参
表单参数
- 表单传输为post请求,http常见的传输格式为四种:
- application/json
- application/x-www-form-urlencoded
- application/xml
- multipart/form-data
- 表单参数可以通过PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded或from-data格式的参数
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>提交页面</title>
</head>
<body>
<form action="http://localhost:8000/form" method="post" action="application/x-www-form-urlencoded">
用户名:<input type="text" name="username" placeholder="请输入你的用户名"> <br>
密 码:<input type="password" name="password" placeholder="请输入你的用户名">
<input type="submit" value="提交">
</form>
</body>
</html>
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.POST("/form", func(c *gin.Context) {
// 获取/form的请求类型,及各参数
types := c.DefaultPostForm("type", "post")
username := c.PostForm("username")
password := c.PostForm("password")
c.String(http.StatusOK, fmt.Sprintf("username:%s,password:%s,type:%s", username, password, types))
})
r.Run(":8000")
}
结果:
上传文件
- multipart/form-data格式用于文件上传
- gin文件上传与原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中
单个文件
- 使用 Request.FormFile 方法获取文件
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传单个文件</title>
</head>
<body>
<form action="http://localhost:8000/upload" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="file" >
<input type="submit" value="提交">
</form>
</body>
</html>
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
// 上传文件
func main() {
r := gin.Default()
//限制上传最大尺寸
r.MaxMultipartMemory = 8 << 20
r.POST("/upload", func(c *gin.Context) {
_, headers, err := c.Request.FormFile("file")
if err != nil {
log.Printf("Error when try to get file: %v", err)
}
//上传特定的文件
//headers.Size 获取文件大小
//if headers.Size > 1024*1024*2 {
// fmt.Println("文件太大了")
// return
//}
//headers.Header.Get("Content-Type")获取上传文件的类型
//if headers.Header.Get("Content-Type") != "image/png" {
// fmt.Println("只允许上传png图片")
// return
//}
c.SaveUploadedFile(headers, "./video/"+headers.Filename)
c.String(http.StatusOK, "上传成功" + headers.Filename)
})
r.Run(":8000")
结果:
多个文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传多个文件</title>
</head>
<body>
<form action="http://localhost:8000/upload" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="files" multiple>
<input type="submit" value="提交">
</form>
</body>
</html>
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 限制表单上传大小 8MB,默认为32MB
r.MaxMultipartMemory = 8 << 20
r.POST("/upload", func(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get err %s", err.Error()))
}
// 获取所有图片
files := form.File["files"]
// 遍历所有图片
for _, file := range files {
// 逐个存
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload err %s", err.Error()))
return
}
}
c.String(200, fmt.Sprintf("upload ok %d files", len(files)))
})
//默认端口号是8080
r.Run(":8000")
}
结果:
路由组
- 用来管理有相同的URL的路由
使用示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
// 路由组
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 路由组1 ,处理GET请求
v1 := r.Group("/v1")
// {} 是书写规范
{
v1.GET("/login", login)
v1.GET("submit", submit)
}
v2 := r.Group("/v2")
{
v2.POST("/login", login)
v2.POST("/submit", submit)
}
r.Run(":8000")
}
func login(c *gin.Context) {
name := c.DefaultQuery("name", "jack")
c.String(200, fmt.Sprintf("hello %s\n", name))
}
func submit(c *gin.Context) {
name := c.DefaultQuery("name", "lily")
c.String(200, fmt.Sprintf("hello %s\n", name))
}
访问失败页面
使用r.NoRoute来设置
例如:
r.NoRoute(func(c *gin.Context) {
c.String(http.StatusNotFound, "sorry,天太冷了,页面跑去钻小被窝了")
})
结果:
gin路由原理
- 我们知道gin的路由是基于httprouter的,所以httprouter中路由的原理也就是gin路由的原理
- httprouter路由原理请参考:简书-Root_808c的httprouter路由原理
gin数据解析和绑定
Json数据解析和绑定
- 客户端传参,Json格式,服务端解析到结构体
- 使用gin.Context.ShouldBindJSON(&json) err将接收到的json数据解析到结构体中
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
- 解析中binding中的required该字段表示必须传参,如果不传参则会报错
表单数据解析和绑定
- 使用gin.Context.Bind(&form) err 将接收到的表单数据解析到结构体中
··<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>表单提交数据解析到结构体</title>
</head>
<body>
<form action="http://localhost:8000/loginForm" method="post" enctype="application/x-www-form-urlencoded">
用户名:<input type="text" name="username"><br>
密 码:<input type="password" name="password"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Login struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// JSON绑定
r.POST("/loginForm", func(c *gin.Context) {
// 声明接收的变量
var form Login
// Bind()默认解析并绑定form格式
// 根据请求头中content-type自动推断
if err := c.Bind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if form.User != "root" || form.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest})
return
}
c.JSON(http.StatusOK, gin.H{"status": http.StatusOK,"body":form})
})
r.Run(":8000")
}
结果:
URI数据解析和绑定
- 使用gin.Context.ShouldBindUri(&login) err 解析uri数据到结构体中
示例:
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// JSON绑定
r.GET("/:user/:password", func(c *gin.Context) {
// 声明接收的变量
var login Login
// Bind()默认解析并绑定form格式
// 根据请求头中content-type自动推断
if err := c.ShouldBindUri(&login); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if login.User != "root" || login.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
结果:
gin 渲染
各种数据格式的响应
- json、结构体、XML、YAML类似于java的properties、ProtoBuf
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 1.json
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "someJSON", "status": http.StatusOK})
})
// 2. 结构体响应
r.GET("/someStruct", func(c *gin.Context) {
var msg struct {
Name string
Message string
Number int
}
msg.Name = "root"
msg.Message = "message"
msg.Number = 123
c.JSON(http.StatusOK, msg)
})
// 3.XML
r.GET("/someXML", func(c *gin.Context) {
c.XML(200, gin.H{"message": "abc"})
})
// 4.YAML响应
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(200, gin.H{"name": "zhangsan"})
})
// 5.protobuf格式,谷歌开发的高效存储读取的工具
// 数组?切片?如果自己构建一个传输格式,应该是什么格式?
//r.GET("/someProtoBuf", func(c *gin.Context) {
// reps := []int64{int64(1), int64(2)}
// // 定义数据
// label := "label"
// // 传protobuf格式数据
// data := &protoexample.Test{
// Label: &label,
// Reps: reps,
// }
// c.ProtoBuf(200, data)
//})
r.Run(":8000")
}
XML页面结果:
HTML模板渲染
- gin支持加载HTML模板, 然后根据模板参数进行配置并返回相应的数据,本质上就是字符串替换
- LoadHTMLGlob()方法可以加载模板文件
html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.title}}</title>
</head>
<body>
<h1>{{.test}}</h1>
</body>
</html>
go文件:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// html模板渲染
func main() {
r := gin.Default()
// 加载模板文件
r.LoadHTMLGlob("static/*")
// 如果项目结构不同,也可以是
// r.LoadHTMLGlob("static/**/*")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是测试", "test": "这是一个测试文件"})
})
r.Run(":8000")
}
结果:
重定向
- 使用 gin.Context.Redirect 进行重定向
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 重定向
func main() {
r := gin.Default()
r.GET("/index", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com")
})
r.Run(":8000")
}
同步异步
- 同步:先执行再响应
- 异步:先响应再执行
- go的协程机制(goroutine)可以方便地实现异步处理
- 另外,在启动新的goroutine时,不应该使用原始上下文,必须使用它的只读副本
package main
import (
"github.com/gin-gonic/gin"
"log"
"time"
)
// 同步异步
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 1.异步
r.GET("/long_async", func(c *gin.Context) {
// 需要搞一个副本
copyContext := c.Copy()
// 异步处理
go func() {
time.Sleep(30 * time.Second)
log.Println("异步执行:" + copyContext.Request.URL.Path)
}()
})
// 2.同步
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(30 * time.Second)
log.Println("同步执行:" + c.Request.URL.Path)
})
r.Run(":8000")
}
结果:
gin 中间件
全局中间件
- 所有请求都需要经过的中间件
package main import ( "fmt" "github.com/gin-gonic/gin" ) // 先定义一个中间件 func MiddleWare() gin.HandlerFunc { return func(c *gin.Context) { fmt.Println("中间件开始执行了") // 设置变量到Context的key中,可以通过Get()取 c.Set("request", "中间件") status := c.Writer.Status() fmt.Println("中间件执行完毕", status) } } func main() { // 1.创建路由 // 默认使用了2个中间件Logger(), Recovery() r := gin.Default() // 注册中间件 r.Use(MiddleWare()) // {}为代码规范 { r.GET("/test", func(c *gin.Context) { // 取值 req, _ := c.Get("request") fmt.Println("request:", req) // 页面接收 c.JSON(200, gin.H{"request": req}) }) } r.Run(":8000") }
结果:
Next() 函数
源码:
// Next should be used only inside middleware.
// Next 应该只在中间件中被使用
// It executes the pending handlers in the chain inside the calling handler.
// 挂起现在正在执行的handlers
// See example in GitHub.
func (c *Context) Next() {
c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
测试:
// 中间件中Next的函数的测试
func main() {
r := gin.New()
mid1 := func(c *gin.Context) {
start := time.Now()
fmt.Println("middleware1 start")
// 注释 or 不注释,查看输入结果
//c.Next()
fmt.Println(time.Since(start))
fmt.Println("middleware1 ending")
}
mid2 := func(c *gin.Context) {
fmt.Println("middleware2 start")
c.Next()
fmt.Println("middleware2 ending")
}
r.Use(mid1)
r.Use(mid2)
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, "hi")
})
r.Run(":8000")
}
结果:
# mid1中的Next()执行
middleware1 start
middleware2 start
middleware2 ending
758.6µs
middleware1 ending
# mid1中的Next()注释上
middleware1 start
# 不加Next则不会出现响应时间
0s
middleware1 ending
middleware2 start
middleware2 ending
可以看出,再加Next函数后mid1会先挂起,等其余的中间件(mid2)执行完再继续执行Next函数后的语句,并且这个过程是一个压栈的过程,也就是说先执行的Next后的语句后被执行。
Next之前的操作一般用来做验证处理,访问是否允许之类的。
Next之后的操作一般是用来做总结处理,比如格式化输出、响应结束时间,响应时长计算之类的。
局部中间件
- 在特殊的路由中指定中间件
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
fmt.Println("中间件开始执行了")
// 设置变量到Context的key中,可以通过Get()取
c.Set("request", "中间件")
// 执行函数
c.Next()
// 中间件执行完后续的一些事情
status := c.Writer.Status()
fmt.Println("中间件执行完毕", status)
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
//局部中间键使用
r.GET("/ce", MiddleWare(), func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 页面接收
c.JSON(200, gin.H{"request": req})
})
r.Run(":8000")
}
结果:
只有在访问"/ce"会执行中间件
会话处理
Cookie
Cookie介绍
- HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否由同一个客户端发出
- Cookie就是解决HTTP协议无状态的方案之一,中文是小甜饼的意思
- Cookie实际上就是服务器保存在浏览器上的一段信息。浏览器有了Cookie之后,每次向服务器发送请求时都会同时将该信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求
- Cookie由服务器创建,并发送给浏览器,最终由浏览器保存
cookie用途
- 测试服务端发送cookie给客户端,客户端请求时携带cookie
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// 服务端要给客户端cookie
r.GET("cookie", func(c *gin.Context) {
// 获取客户端是否携带cookie
cookie, err := c.Cookie("key_cookie")
if err != nil {
cookie = "NotSet"
// 给客户端设置cookie
// maxAge int, 单位为秒
// path,cookie所在目录
// domain string,域名
// secure 是否智能通过https访问
// httpOnly bool 是否允许别人通过js获取自己的cookie
c.SetCookie("key_cookie", "value_cookie", 60, "/",
"localhost", false, true)
}
fmt.Printf("cookie的值是: %s\n", cookie)
})
r.Run(":8000")
}
// 其中 SetCookie方法的参数的含义为
// maxAge int, 单位为秒
// path,cookie所在目录
// domain string,域名
// secure 是否智能通过https访问
// httpOnly bool 是否允许别人通过js获取自己的cookie
SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
结果:
cookie的值是: NotSet
这样便有了cookie的值
Cookie缺点
- 不安全,明文(就像上面的示例的结果)
- 增加带宽消耗
- 可以被禁用
- cookie数量有上限(每个浏览器不同)
Sessions
- 简单的API:将其用作设置签名(以及可选的加密)cookie的简便方法。
- 内置的后端可将session存储在cookie或文件系统中。
- Flash消息:一直持续读取的session值。
- 切换session持久性(又称“记住我”)和设置其他属性的便捷方法。
- 旋转身份验证和加密密钥的机制。
- 每个请求有多个session,即使使用不同的后端也是如此。
- 自定义session后端的接口和基础结构:可以使用通用API检索并批量保存来自不同商店的session。
示例:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
// 初始化一个cookie存储对象
// something-very-secret应该是一个你自己的密匙,只要不被别人知道就行
var store = sessions.NewCookieStore([]byte("something-very-secret"))
func main() {
http.HandleFunc("/save", SaveSession)
http.HandleFunc("/get", GetSession)
http.HandleFunc("/del", DelSession)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("HTTP server failed,err:", err)
return
}
}
func SaveSession(w http.ResponseWriter, r *http.Request) {
// Get a session. We're ignoring the error resulted from decoding an
// existing session: Get() always returns a session, even if empty.
// 获取一个session对象,session-name是session的名字
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 在session中存储值
session.Values["foo"] = "bar"
session.Values[42] = 43
// 保存更改
session.Save(r, w)
}
func GetSession(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
foo := session.Values["foo"]
fmt.Println(foo)
}
func DelSession(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 将session的时间设为小于零的数就是删除
session.Options.MaxAge = -1
// 保存更改
session.Save(r, w)
}
结果:
# 先save后get,可以取到值
bar
# 然后del再get,就取不到值了
<nil>
参考: