Gin
環境:https://goproxy.cn,driect
github.com/gin-gonic/gin
介紹
Gin 是一個用 Go (Golang) 撰寫的 Web 框架, 它具有類似 martini 的 API,性能要好得多,多虧了 httprouter,速度提高了 40 倍, 如果您需要性能和良好的生產力,您一定會喜歡 Gin,
在本節中,我們將介紹 Gin 是什么,它解決了哪些問題,以及它如何幫助你的專案,
或者, 如果你已經準備在專案中使用 Gin,請訪問快速入門.
原始碼分析
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
實作
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
- 通過物件池來減少記憶體申請和GC回收的消耗
- 取出來要用的時候再初始化
特性
快速
基于 Radix 樹的路由,小記憶體占用,沒有反射,可預測的 API 性能,
支持中間件
傳入的 HTTP 請求可以由一系列中間件和最終操作來處理, 例如:Logger,Authorization,GZIP,最終操作 DB,
Crash 處理
Gin 可以 catch 一個發生在 HTTP 請求中的 panic 并 recover 它,這樣,你的服務器將始終可用,例如,你可以向 Sentry 報告這個 panic!
JSON 驗證
Gin 可以決議并驗證請求的 JSON,例如檢查所需值的存在,
路由組
更好地組織路由,是否需要授權,不同的 API 版本…… 此外,這些組可以無限制地嵌套而不會降低性能,
錯誤管理
Gin 提供了一種方便的方法來收集 HTTP 請求期間發生的所有錯誤,最終,中間件可以將它們寫入日志檔案,資料庫并通過網路發送,
內置渲染
Gin 為 JSON,XML 和 HTML 渲染提供了易于使用的 API,
可擴展性
新建一個中間件非常簡單,去查看示例代碼吧,
快速入門
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(":8000")// 監聽并在 0.0.0.0:8080 上啟動服務
}
gin.Default()
默認使用了Logger
和Recover
中間件Logger
是負責進行列印輸出日志的中間件,方便開發者進行程式的除錯Recover
如果程式執行程序中遇到了panic
中斷了服務,則Recover
會恢復程式的運行并回傳500的內部錯誤,
Engine
type Engine struct {
RouterGroup
RedirectTrailingSlash bool
RedirectFixedPath bool
HandleMethodNotAllowed bool
ForwardedByClientIP bool
AppEngine bool
UseRawPath bool
UnescapePathValues bool
RemoveExtraSlash bool
RemoteIPHeaders []string
TrustedPlatform string
MaxMultipartMemory int64
delims render.Delims
secureJSONPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
maxParams uint16
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet
}
請求處理
HTTP 協議的 8 種請求型別介紹
HTTP 協議中共定義了八種方法或者叫“動作”來表明對Request-URI
指定的資源的不同操作方式,具體介紹如下:
OPTIONS
:回傳服務器針對特定資源所支持的HTTP請求方法,也可以利用向Web服務器發送'*'的請求來測驗服務器的功能性,HEAD
:向服務器索要與GET請求相一致的回應,只不過回應體將不會被回傳,這一方法可以在不必傳輸整個回應內容的情況下,就可以獲取包含在回應訊息頭中的元資訊,GET
:向特定的資源發出請求,POST
:向指定資源提交資料進行處理請求(例如提交表單或者上傳檔案),資料被包含在請求體中,POST請求可能會導致新的資源的創建和/或已有資源的修改,PUT
:向指定資源位置上傳其最新內容,DELETE
:請求服務器洗掉 Request-URI 所標識的資源,TRACE
:回顯服務器收到的請求,主要用于測驗或診斷,CONNECT
:HTTP/1.1 協議中預留給能夠將連接改為管道方式的代理服務器,
通用處理
Handle方法
func (group *RouterGroup)Handle(httpMethod,relativePath string,handler...Handler)
httpMethod
:表示要處理的HTTP請求型別,8種請求方式之一relativePath
:表示要決議的介面,由開發者定義handlers
:處理對應的請求的代碼定義
Restful風格的API
- gin支持Restful風格的API
- 即Representational State Transfer的縮寫,直接翻譯的意思是”表現層狀態轉化”,是一種互聯網應用程式的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
GET請求處理
路徑引數- /:xx - Param("xx")
對于類似這樣的請求:http://127.0.0.1:8080/index/12,那么如何獲取最后路徑中12的值呢?
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func Index(ctx *gin.Context) {
id := ctx.Param("id")
fmt.Println(id)
ctx.String(http.StatusOK, "success!")
}
func main() {
router := gin.Default()
// 路徑引數獲取,如:http://127.0.0.1:8080/index/12,獲取12
router.GET("/index/:id", Index)
router.Run(":8080")
}
在掛載路由時需要通過":"來進行匹配,然后在視圖函式中通過ctx.Param方法獲取,
查詢引數-/hello?xx=..? -Query()
對于類似這樣的請求:http://127.0.0.1:8080/index1?id=12,那么如何獲取最后路徑中12的值呢?
1、ctx.Query
傳參:http://127.0.0.1:8080/index1?id=12
路由:router.GET("/index1", Index1)
視圖函式獲取:ctx.Query("id")
2、ctx.DefaultQuery
傳參:http://127.0.0.1:8080/index2
路由:router.GET("/index2", Index2)
視圖函式獲取:ctx.DefaultQuery("id", "0")
如果沒有獲取到id,就得到默認值0.
3、ctx.QueryArray
傳參:http://127.0.0.1:8080/index3?id=1,2,3,4,5
路由:router.GET("/index3", Index3)
視圖函式獲取:ctx.QueryArray("id")
4、ctx.QueryMap
傳參:http://127.0.0.1:8080/index4?user[name]="lily"&user[age]=15
路由:router.GET("/index4", Index4)
視圖函式獲取:ctx.QueryMap("user")
通用引數匹配 /user/:name/*action
當我們需要動態引數的路由時,如 /user/:id,通過呼叫不同的引數 :id 動態獲取相應的用戶資訊,其中 /user/:id/*type
,*type
的引數為可選,
package main
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
//截取/
action = strings.Trim(action, "/")
c.String(http.StatusOK, name+" is "+action)
})
//默認為監聽8080埠
r.Run(":8000")
}
正常的結果:
http://localhost:8080/api/hjz/HJZ114152
hjz is HJZ114152
注釋掉:Trim()后
http://localhost:8080/api/hjz/HJZ114152
hjz is /HJZ114152
POST請求處理
表單傳輸為post請求,http常見的傳輸格式為四種:
- application/json
- application/x-www-form-urlencoded
- application/xml
- multipart/form-data
表單引數可以通過PostForm()方法獲取,該方法默認決議的是x-www-form-urlencoded或from-data格式的引數
普通方式提交表單-post -PostForm("xxx")
1、ctx.PostForm
- 表單
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/post_index" method="post">
<p>用戶名:<input type="text" name="username"></p>
<p>密 碼:<input type="password" name="password"></p>
<p><input type="submit"></p>
</form>
</body>
</html>
- 后臺處理
...
func PostIndex(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
fmt.Printf("用戶名:%s, 密碼:%s", username, password)
ctx.String(http.StatusOK, "提交成功!")
}
...
2、ctx.PostFormMap
- 表單
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/post_index1" method="post">
<p>用戶名:<input type="text" name="user[username]"></p>
<p>密 碼:<input type="password" name="user[password]"></p>
<p><input type="submit"></p>
</form>
</body>
</html>
- 后臺處理
...
func PostIndex1(ctx *gin.Context) {
userMap := ctx.PostFormMap("user")
fmt.Println(userMap)
ctx.String(http.StatusOK, "提交成功!")
}
...
3、ctx.DefaultPostForm、ctx.PostFormArray
- 表單
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/post_index2" method="post">
<p>用戶名:<input type="text" name="username"></p>
<p>密 碼:<input type="password" name="password"></p>
<p>愛 好:
讀書<input type="checkbox" name="hobby" value="https://www.cnblogs.com/HJZ114152/archive/2023/04/17/1">
看電影<input type="checkbox" name="hobby" value="https://www.cnblogs.com/HJZ114152/archive/2023/04/17/2">
音樂<input type="checkbox" name="hobby" value="https://www.cnblogs.com/HJZ114152/archive/2023/04/17/3">
</p>
<p><input type="submit"></p>
</form>
</body>
</html>
- 后臺處理
...
func PostIndex2(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
age := ctx.DefaultPostForm("age", "0")
hobby := ctx.PostFormArray("hobby")
fmt.Printf("用戶名:%s, 密碼:%s, 年齡:%s, 愛好:%s", username, password, age, hobby)
ctx.String(http.StatusOK, "提交成功!")
}
...
例子
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func GetIndex(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", nil)
}
func PostIndex(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
fmt.Printf("用戶名:%s, 密碼:%s", username, password)
ctx.String(http.StatusOK, "提交成功!")
}
func GetIndex1(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index1.html", nil)
}
func PostIndex1(ctx *gin.Context) {
userMap := ctx.PostFormMap("user")
fmt.Println(userMap)
ctx.String(http.StatusOK, "提交成功!")
}
func GetIndex2(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index2.html", nil)
}
func PostIndex2(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
age := ctx.DefaultPostForm("age", "0")
hobby := ctx.PostFormArray("hobby")
fmt.Printf("用戶名:%s, 密碼:%s, 年齡:%s, 愛好:%s", username, password, age, hobby)
ctx.String(http.StatusOK, "提交成功!")
}
func main() {
router := gin.Default()
router.LoadHTMLGlob("template/*")
// ctx.PostForm
router.GET("/get_index", GetIndex)
router.POST("/post_index", PostIndex)
// ctx.PostFormMap
router.GET("/get_index1", GetIndex1)
router.POST("/post_index1", PostIndex1)
// ctx.DefaultPostForm、ctx.PostFormArray
router.GET("/get_index2", GetIndex2)
router.POST("/post_index2", PostIndex2)
router.Run(":8080")
}
Ajax方式提交表單
ajax的后臺處理邏輯與普通的表單的提交的處理方式基本相同,只不過在回傳的時候需要回傳json資料,前臺使用回呼函式進行處理,
- 前臺
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://www.cnblogs.com/static/js/jquery-3.6.0.js"></script>
</head>
<body>
<form>
<p>用戶名:<input id="username" type="text"></p>
<p>密碼:<input id="password" type="password"></p>
<p><input type="button" value="https://www.cnblogs.com/HJZ114152/archive/2023/04/17/提交" id="btn_submit"></p>
</form>
<script>
var btn = document.getElementById("btn_submit")
btn.onclick = function (ev) {
var username = document.getElementById("username").value
var password = document.getElementById("password").value
$.ajax({
url: '/post_index',
type: 'POST',
data: {
username: username,
password: password
},
success: function (data) {
alert(data) // 回應成功的回呼函式
},
fail: function (data) {
}
})
}
</script>
</body>
</html>
- 后臺
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func GetIndex(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", nil)
}
func PostIndex(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
fmt.Println(username, password)
data := map[string]interface{}{
"code": 2000,
"message": "成功",
}
ctx.JSON(http.StatusOK, data)
}
func main() {
router := gin.Default()
router.LoadHTMLGlob("template/*")
router.Static("/static", "static")
router.GET("/get_index", GetIndex)
router.POST("/post_index", PostIndex)
router.Run(":8080")
}
引數系結
無論時get請求的引數還是post請求的請求體,在后臺都需要通過對應的方法來獲取對應引數的值,那么有沒有一種方式能夠讓我們定義好請求資料的格式,然后自動進行獲取,這里可以通過引數系結的方式來進行處理,它能夠基于請求自動提取JSON、form表單和QueryString型別的資料,并把值系結到指定的結構體物件,
這里以get請求的查詢引數為例:
- 請求格式
http://127.0.0.1:8080/index?username=%22llkk%22&password=%22123%22
- 后臺處理
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type User struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
}
func Index(ctx *gin.Context) {
var user User
err := ctx.ShouldBind(&user)
fmt.Println(err)
fmt.Println(user.Username, user.Password) // "llkk" "123"
ctx.String(http.StatusOK, "success")
}
func main() {
router := gin.Default()
router.GET("/index", Index)
router.Run(":8080")
}
檔案處理
上傳單個檔案
- multipart/form-data格式用于檔案上傳
- gin檔案上傳與原生的net/http方法類似,不同在于gin把原生的request封裝到c.Request中
<!DOCTYPE html>
<html lang="en">
<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>Document</title>
</head>
<body>
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
上傳檔案:<input type="file" name="file" >
<input type="submit" value="https://www.cnblogs.com/HJZ114152/archive/2023/04/17/提交">
</form>
</body>
</html>
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//限制上傳最大尺寸
r.MaxMultipartMemory = 8 << 20
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.String(500, "上傳圖片出錯")
}
// c.JSON(200, gin.H{"message": file.Header.Context})
c.SaveUploadedFile(file, file.Filename)
c.String(http.StatusOK, file.Filename)
})
r.Run()
}
上傳多個檔案
<!DOCTYPE html>
<html lang="en">
<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>Document</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="https://www.cnblogs.com/HJZ114152/archive/2023/04/17/提交">
</form>
</body>
</html>
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"fmt"
)
// gin的helloWorld
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")
}
快速分析
GET
get請求的引數會放在地址欄里,用明文的形式提交給后臺 .
例如:
http://127.0.0.1:8081/path/123
r.GET("/path/:id", func(c *gin.Context) {
c.JSON(200, gin.H{
"success": true,
})
})
獲得URL里面的引數:
http://127.0.0.1:8081/path/123?user=abc&pwd=123456
r.GET("/path/:id", func(c *gin.Context) {
id := c.Param("id") //獲取占位運算式后面的引數
//user和pwd放在地址欄后面,所以叫query傳參
user := c.Query("user") //
pwd := c.Query("pwd")
c.JSON(200, gin.H{
"id": id,
"user": user,
"pwd": pwd,
})
})
設定一個引數的默認值:
r.GET("/path/:id", func(c *gin.Context) {
id := c.Param("id") //獲取占位運算式后面的引數
//user和pwd放在地址欄后面,所以叫query傳參
user := c.DefaultQuery("user", "kaka") //設定user的默認值為kaka
pwd := c.Query("pwd")
c.JSON(200, gin.H{
"id": id,
"user": user,
"pwd": pwd,
})
})
POST
post的請求引數放在form或者body里,form即表單,body是以當前最流行的json格式進行互動,
r.POST("/path", func(c *gin.Context) {
user := c.DefaultPostForm("user", "aaa")
pwd := c.PostForm("pwd")
c.JSON(200, gin.H{
"user": user,
"pwd": pwd,
})
})
Delete
delete請求一般為uri,同樣也可以用body
r.DELETE("/path/:id", func(c *gin.Context) {
id := c.Param("id") //獲取占位運算式后面的引數
c.JSON(200, gin.H{
"id": id,
})
})
delete請求實際作業中用的不多,傳參和取參方法和get類似
PUT
引數在form、body或者uri里
r.PUT("/path", func(c *gin.Context) {
user := c.DefaultPostForm("user", "aaa")
pwd := c.PostForm("pwd")
c.JSON(200, gin.H{
"user": user,
"pwd": pwd,
})
})
put傳參和獲取引數的方法和post基本一樣
回傳資料
[]byte
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "請求路徑: "+context.FullPath()
fmt.Println(fullPath)
context.Writer.Writer([]byte(fullPath))
})
engine.Run()
...
呼叫了http.ResponseWriter中包含的方法
string
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "請求路徑: "+context.FullPath()
fmt.Println(fullPath)
context.Writer.WriterString([]byte(fullPath))
})
engine.Run()
...
JSON
map
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "請求路徑: "+context.FullPath()
fmt.Println(fullPath)
context.JSON(200,map[string]interface{}{
"code":1,
"message":"OK",
"data":FullPath
})
})
engine.Run()
...
結構體
type Resopnse type{
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
...
engine := gin.Defaut()
engine.GET("/hello",func(context *gin.Context){
fullPath := "請求路徑: "+context.FullPath()
fmt.Println(fullPath)
context.JSON(200,&Resopnse{
Code:1,
Message:"OK",
Data:FullPath
})
})
engine.Run()
...
資料決議
Json 資料決議和系結
ShouldBindJSON("&xx")
- 客戶端傳參,后端接收并決議到結構體
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("loginJSON", func(c *gin.Context) {
// 宣告接收的變數
var json Login
// 將request的body中的資料,自動按照json格式決議到結構體
if err := c.ShouldBindJSON(&json); err != nil {
// 回傳錯誤資訊
// gin.H封裝了生成json資料的工具
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判斷用戶名密碼是否正確
if json.User != "root" || json.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
表單資料決議和系結
Bind("&xx")
<!DOCTYPE html>
<html lang="en">
<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>Document</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">
<input type="submit" value="https://www.cnblogs.com/HJZ114152/archive/2023/04/17/提交">
</form>
</body>
</html>
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 定義接收資料的結構體
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": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
URI資料決議和系結
ShouldBindUri()
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 定義接收資料的結構體
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.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")
}
視圖渲染
各種資料格式的回應
- json、結構體、XML、YAML類似于java的properties、ProtoBuf
JSON
// 1.json
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "someJSON", "status": 200})
})
結構體
// 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(200, msg)
})
XML
// 3.XML
r.GET("/someXML", func(c *gin.Context) {
c.XML(200, gin.H{"message": "abc"})
})
YAML
// 4.YAML回應
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(200, gin.H{"name": "zhangsan"})
})
protoduf
// 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)
})
Gin框架使用HTML模板渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("./templates/*")
r.GET("/demo", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "test.html", gin.H{
"name": "admin",
"pwd": "123456",
})
})
r.Run()
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
賬號是:{{.name}}<br>
密碼是:{{.pwd}}
</body>
</html>
重定向
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/index", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.5lmh.com")
})
r.Run()
}
同步異步
- goroutine機制可以方便地實作異步處理
- 另外,在啟動新的goroutine時,不應該使用原始背景關系,必須使用它的只讀副本
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
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(3 * time.Second)
log.Println("異步執行:" + copyContext.Request.URL.Path)
}()
})
// 2.同步
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(3 * time.Second)
log.Println("同步執行:" + c.Request.URL.Path)
})
r.Run(":8000")
}
日志的列印上:
[GIN] 2022/08/01 - 10:48:56 | 200 | 0s | ::1 | GET "/long_async"
2022/08/01 10:48:59 異步執行:/long_async
2022/08/01 10:49:17 同步執行:/long_sync
[GIN] 2022/08/01 - 10:49:17 | 200 | 3.0071153s | ::1 | GET "/long_sync"
中間件
全域中間件
- 所有請求都經過此中間件
package main
import (
"fmt"
"time"
"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", "中間件")
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.Use(MiddleWare())
// {}為了代碼規范
{
r.GET("/ce", func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 頁面接收
c.JSON(200, gin.H{"request": req})
})
}
r.Run()
}
中間件開始執行了
中間件執行完畢 200
time: 516.1μs
request: 中間件
[GIN] 2022/10/02 - 14:56:20 | 200 | 516.1μs | ::1 | GET "/ce"
Next()方法
package main
import (
"fmt"
"time"
"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.Use(MiddleWare())
// {}為了代碼規范
{
r.GET("/ce", func(c *gin.Context) {
// 取值
req, _ := c.Get("request")
fmt.Println("request:", req)
// 頁面接收
c.JSON(200, gin.H{"request": req})
})
}
r.Run()
}
中間件開始執行了
request: 中間件
中間件執行完畢 200
time: 235.9μs
[GIN] 2022/10/02 - 14:57:46 | 200 | 754.8μs | ::1 | GET "/ce"
區域中間件
package main
import (
"fmt"
"time"
"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()
}
在中間件中使用Goroutine
當在中間件或 handler 中啟動新的 Goroutine 時,不能使用原始的背景關系,必須使用只讀副本,
package main
import (
"github.com/gin-gonic/gin"
"log"
"time"
)
func main() {
r := gin.Default()
r.GET("/long_async", func(c *gin.Context) {
// 創建在 goroutine 中使用的副本
tmp := c.Copy()
go func() {
// 用 time.Sleep() 模擬一個長任務,
time.Sleep(5 * time.Second)
// 請注意您使用的是復制的背景關系 "tmp",這一點很重要
log.Println("Done! in path " + tmp.Request.URL.Path)
}()
})
r.GET("/long_sync", func(c *gin.Context) {
// 用 time.Sleep() 模擬一個長任務,
time.Sleep(5 * time.Second)
// 因為沒有使用 goroutine,不需要拷貝背景關系
log.Println("Done! in path " + c.Request.URL.Path)
})
r.Run()
}
會話控制
Cookie介紹
- HTTP是無狀態協議,服務器不能記錄瀏覽器的訪問狀態,也就是說服務器不能區分兩次請求是否由同一個客戶端發出
- Cookie就是解決HTTP協議無狀態的方案之一,中文是小甜餅的意思
- Cookie實際上就是服務器保存在瀏覽器上的一段資訊,瀏覽器有了Cookie之后,每次向服務器發送請求時都會同時將該資訊發送給服務器,服務器收到請求后,就可以根據該資訊處理請求
- Cookie由服務器創建,并發送給瀏覽器,最終由瀏覽器保存
Cookie的用途
- 測驗服務端發送cookie給客戶端,客戶端請求時攜帶cookie
Cookie的使用
- 測驗服務端發送cookie給客戶端,客戶端請求時攜帶cookie
package main
import (
"github.com/gin-gonic/gin"
"fmt"
)
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")
}
Gin設定日志
在Gin框架中記錄日志方法如下
package main
import (
"io"
"os"
"github.com/gin-gonic/gin"
)
func main() {
// 禁用控制臺顏色,將日志寫入檔案時不需要控制臺顏色,
gin.DisableConsoleColor()
// 記錄到檔案,
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 如果需要同時將日志寫入檔案和控制臺,請使用以下代碼,
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
r.Run()
}
以上代碼執行結果如下
Cookie的聯系
- 模擬實作權限驗證中間件
- 有2個路由,login和home
- login用于設定cookie
- home是訪問查看資訊的請求
- 在請求home之前,先跑中間件代碼,檢驗是否存在cookie
- 訪問home,會顯示錯誤,因為權限校驗未通過
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func AuthMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
// 獲取客戶端cookie并校驗
if cookie, err := c.Cookie("abc"); err == nil {
if cookie == "123" {
c.Next()
return
}
}
// 回傳錯誤
c.JSON(http.StatusUnauthorized, gin.H{"error": "err"})
// 若驗證不通過,不再呼叫后續的函式處理
c.Abort()
return
}
}
func main() {
// 1.創建路由
r := gin.Default()
r.GET("/login", func(c *gin.Context) {
// 設定cookie
c.SetCookie("abc", "123", 60, "/",
"localhost", false, true)
// 回傳資訊
c.String(200, "Login success!")
})
r.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
c.JSON(200, gin.H{"data": "home"})
})
r.Run(":8000")
}
Cookie的缺點
- 不安全,明文
- 增加帶寬消耗
- 可以被禁用
- cookie有上限
Sessions
gorilla/sessions為自定義session后端提供cookie和檔案系統session以及基礎結構,
主要功能是:
- 簡單的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)
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)
}
洗掉session的值:
// 洗掉
// 將session的最大存盤時間設定為小于零的數即為洗掉
session.Options.MaxAge = -1
session.Save(r, w)
引數驗證
結構體驗證
用gin框架的資料驗證,可以不用決議資料,減少if else,會簡潔許多,
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
//Person ..
type Person struct {
//不能為空并且大于10
Age int `form:"age" binding:"required,gt=10"`
Name string `form:"name" binding:"required"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}
func main() {
r := gin.Default()
r.GET("/5lmh", func(c *gin.Context) {
var person Person
if err := c.ShouldBind(&person); err != nil {
c.String(500, fmt.Sprint(err))
return
}
c.String(200, fmt.Sprintf("%#v", person))
})
r.Run()
}
自定義驗證
都在代碼里自己看吧
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"gopkg.in/go-playground/validator.v9"
)
/*
對系結決議到結構體上的引數,自定義驗證功能
比如我們需要對URL的接受引數進行判斷,判斷用戶名是否為root如果是root通過否則回傳false
*/
type Login struct {
User string `uri:"user" validate:"checkName"`
Pssword string `uri:"password"`
}
// 自定義驗證函式
func checkName(fl validator.FieldLevel) bool {
if fl.Field().String() != "root" {
return false
}
return true
}
func main() {
r := gin.Default()
validate := validator.New()
r.GET("/:user/:password", func(c *gin.Context) {
var login Login
//注冊自定義函式,與struct tag關聯起來
err := validate.RegisterValidation("checkName", checkName)
if err := c.ShouldBindUri(&login); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = validate.Struct(login)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
fmt.Println(err)
}
return
}
fmt.Println("success")
})
r.Run()
}
示例2:
package main
import (
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
// Booking contains binded and validated data.
type Booking struct {
//定義一個預約的時間大于今天的時間
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
//gtfield=CheckIn退出的時間大于預約的時間
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
//field.Interface().(time.Time)獲取引數值并且轉換為時間格式
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Unix() > date.Unix() {
return false
}
}
return true
}
func main() {
route := gin.Default()
//注冊驗證
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//系結第一個引數是驗證的函式第二個引數是自定義的驗證函式
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/5lmh", getBookable)
route.Run()
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
// curl -X GET "http://localhost:8080/5lmh?check_in=2019-11-07&check_out=2019-11-20"
// curl -X GET "http://localhost:8080/5lmh?check_in=2019-09-07&check_out=2019-11-20"
// curl -X GET "http://localhost:8080/5lmh?check_in=2019-11-07&check_out=2019-11-01"
自定義驗證v10
介紹
Validator 是基于 tag(標記)實作結構體和單個欄位的值驗證庫,它包含以下功能:
- 使用驗證 tag(標記)或自定義驗證器進行跨欄位和跨結構體驗證,
- 關于 slice、陣列和 map,允許驗證多維欄位的任何或所有級別,
- 能夠深入 map 鍵和值進行驗證,
- 通過在驗證之前確定介面的基礎型別來處理型別介面,
- 處理自定義欄位型別(如 sql 驅動程式 Valuer),
- 別名驗證標記,它允許將多個驗證映射到單個標記,以便更輕松地定義結構體上的驗證,
- 提取自定義的欄位名稱,例如,可以指定在驗證時提取 JSON 名稱,并在生成的 FieldError 中使用該名稱,
- 可自定義 i18n 錯誤訊息,
- Web 框架 gin 的默認驗證器,
安裝:
使用 go get:
go get github.com/go-playground/validator/v10
然后將 Validator 包匯入到代碼中:
import "github.com/go-playground/validator/v10"
變數驗證
Var 方法使用 tag(標記)驗證方式驗證單個變數,
func (*validator.Validate).Var(field interface{}, tag string) error
它接收一個 interface{} 空介面型別的 field 和一個 string 型別的 tag,回傳傳遞的非法值得無效驗證錯誤,否則將 nil 或 ValidationErrors 作為錯誤,如果錯誤不是 nil,則需要斷言錯誤去訪問錯誤陣列,例如:
validationErrors := err.(validator.ValidationErrors)
如果是驗證陣列、slice 和 map,可能會包含多個錯誤,
示例代碼:
func main() {
validate := validator.New()
// 驗證變數
email := "admin#admin.com"
email := ""
err := validate.Var(email, "required,email")
if err != nil {
validationErrors := err.(validator.ValidationErrors)
fmt.Println(validationErrors)
// output: Key: '' Error:Field validation for '' failed on the 'email' tag
// output: Key: '' Error:Field validation for '' failed on the 'required' tag
return
}
}
結構體驗證
結構體驗證結構體公開的欄位,并自動驗證嵌套結構體,除非另有說明,
func (*validator.Validate).Struct(s interface{}) error
它接收一個 interface{} 空介面型別的 s,回傳傳遞的非法值得無效驗證錯誤,否則將 nil 或 ValidationErrors 作為錯誤,如果錯誤不是 nil,則需要斷言錯誤去訪問錯誤陣列,例如:
validationErrors := err.(validator.ValidationErrors)
實際上,Struct 方法是呼叫的 StructCtx 方法,因為本文不是原始碼講解,所以此處不展開贅述,如有興趣,可以查看原始碼,
示例代碼:
func main() {
validate = validator.New()
type User struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name" validate:"required"`
Gender string `json:"gender" validate:"required,oneof=man woman"`
Age uint8 `json:"age" validate:"required,gte=0,lte=130"`
Email string `json:"email" validate:"required,email"`
}
user := &User{
ID: 1,
Name: "frank",
Gender: "boy",
Age: 180,
Email: "[email protected]",
}
err = validate.Struct(user)
if err != nil {
validationErrors := err.(validator.ValidationErrors)
// output: Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
// fmt.Println(validationErrors)
fmt.Println(validationErrors.Translate(trans))
return
}
}
細心的讀者可能已經發現,錯誤輸出資訊并不友好,錯誤輸出資訊中的欄位不僅沒有使用備用名(首字母小寫的欄位名),也沒有翻譯為中文,通過改動代碼,使錯誤輸出資訊變得友好,
注冊一個函式,獲取結構體欄位的備用名稱:
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return "j"
}
return name
})
錯誤資訊翻譯為中文:
zh := zh.New()
uni = ut.New(zh)
trans, _ := uni.GetTranslator("zh")
_ = zh_translations.RegisterDefaultTranslations(validate, trans)
標簽
通過以上章節的內容,讀者應該已經了解到 Validator 是一個基于 tag(標簽),實作結構體和單個欄位的值驗證庫,
本章節列舉一些比較常用的標簽:
標簽 | 描述 |
---|---|
eq | 等于 |
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
ne | 不等于 |
max | 最大值 |
min | 最小值 |
oneof | 其中一個 |
required | 必需的 |
unique | 唯一的 |
isDefault | 默認值 |
len | 長度 |
郵箱格式 |
轉自: Golang語言開發堆疊
多語言翻譯驗證
當業務系統對驗證資訊有特殊需求時,例如:回傳資訊需要自定義,手機端回傳的資訊需要是中文而pc端發揮回傳的資訊需要時英文,如何做到請求一個介面滿足上述三種情況,
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
"github.com/go-playground/locales/zh_Hant_TW"
ut "github.com/go-playground/universal-translator"
"gopkg.in/go-playground/validator.v9"
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
zh_translations "gopkg.in/go-playground/validator.v9/translations/zh"
zh_tw_translations "gopkg.in/go-playground/validator.v9/translations/zh_tw"
)
var (
Uni *ut.UniversalTranslator
Validate *validator.Validate
)
type User struct {
Username string `form:"user_name" validate:"required"`
Tagline string `form:"tag_line" validate:"required,lt=10"`
Tagline2 string `form:"tag_line2" validate:"required,gt=1"`
}
func main() {
en := en.New()
zh := zh.New()
zh_tw := zh_Hant_TW.New()
Uni = ut.New(en, zh, zh_tw)
Validate = validator.New()
route := gin.Default()
route.GET("/5lmh", startPage)
route.POST("/5lmh", startPage)
route.Run(":8080")
}
func startPage(c *gin.Context) {
//這部分應放到中間件中
locale := c.DefaultQuery("locale", "zh")
trans, _ := Uni.GetTranslator(locale)
switch locale {
case "zh":
zh_translations.RegisterDefaultTranslations(Validate, trans)
break
case "en":
en_translations.RegisterDefaultTranslations(Validate, trans)
break
case "zh_tw":
zh_tw_translations.RegisterDefaultTranslations(Validate, trans)
break
default:
zh_translations.RegisterDefaultTranslations(Validate, trans)
break
}
//自定義錯誤內容
Validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
//這塊應該放到公共驗證方法中
user := User{}
c.ShouldBind(&user)
fmt.Println(user)
err := Validate.Struct(user)
if err != nil {
errs := err.(validator.ValidationErrors)
sliceErrs := []string{}
for _, e := range errs {
sliceErrs = append(sliceErrs, e.Translate(trans))
}
c.String(200, fmt.Sprintf("%#v", sliceErrs))
}
c.String(200, fmt.Sprintf("%#v", "user"))
}
正確的鏈接:
http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=33&locale=zh
http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=3&locale=en 回傳英文的驗證資訊
http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=3&locale=zh 回傳中文的驗證資訊
查看更多的功能可以查看官網 gopkg.in/go-playground/validator.v9
檔案操作
Gin 并沒有提供檔案的創建,洗掉,讀寫這個操作的專門的介面,所以采用的是常用的ioutil這個包進行檔案的讀寫操作,使用os這個包進行檔案的創建和洗掉,
檔案的創建,寫入內容,讀取內容,洗掉.(此實體使用的是txt檔案):
-controller
+file.go
-router
+router.go
main.go
//檔案的創建洗掉和讀寫
router.GET("/cont/filerw", controllers.Filerwhtml) //獲取檔案api操作資訊
router.POST("/cont/addfile", controllers.FilerCreate) //創建檔案
router.POST("/cont/writefile", controllers.FilerWrite) //寫入檔案
router.POST("/cont/readfile", controllers.FilerRead) //讀取檔案
router.POST("/cont/deletefile", controllers.FilerDelete) //洗掉檔案
package controllers
import (
"fmt"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"os"
)
// 定義接收資料的結構體
type FileData struct {
// binding:"required"修飾的欄位,若接收為空值,則報錯,是必須欄位
FileName string `form:"filename" json:"filename" uri:"filename" xml:"filename" binding:"required"`
Content string `form:"content" json:"content" uri:"content" xml:"content"`
}
//檔案操作介面資訊
type Data struct {
Api string `json:"api"`
Params string `json:"params"`
Remark string `json:"remark"`
}
/**檔案讀寫操作介面資訊**/
func Filerwhtml(c *gin.Context) {
list := []Data{
Data{
"/cont/addfile",
"filename",
"創建檔案",
},
Data{
"/cont/writefile",
"filename,content",
"寫入檔案",
},
Data{
"/cont/readfile",
"filename",
"讀取檔案",
},
Data{
"/cont/deletefile",
"filename",
"洗掉檔案",
},
}
//回傳結果
c.JSON(http.StatusOK, gin.H{"code": 0, "list": list})
return
}
創建檔案
/**創建檔案**/
func FilerCreate(c *gin.Context) {
// 宣告接收的變數
var data FileData
// 將request的body中的資料,自動按照json格式決議到結構體
if err := c.ShouldBindJSON(&data); err != nil {
// 回傳錯誤資訊
// gin.H封裝了生成json資料的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
//創建檔案
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName)
f, err := os.Create(path)
fmt.Print(path)
if err != nil {
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "創建檔案失敗"})
fmt.Print(err.Error())
return
}
defer f.Close()
//回傳結果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "創建成功", "filename": data.FileName})
return
}
寫入檔案
/**將內容寫入檔案**/
func FilerWrite(c *gin.Context) {
// 宣告接收的變數
var data FileData
// 將request的body中的資料,自動按照json格式決議到結構體
if err := c.ShouldBindJSON(&data); err != nil {
// 回傳錯誤資訊
// gin.H封裝了生成json資料的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
//需要寫入到檔案的內容
content := data.Content
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName)
d1 := []byte(content)
err := ioutil.WriteFile(path, d1, 0644)
fmt.Print(path)
if err != nil {
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "寫入內容失敗"})
fmt.Print(err.Error())
return
}
//回傳結果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "寫入內容成功", "filename": data.FileName, "content": content})
return
}
讀取檔案
/**讀取檔案內容**/
func FilerRead(c *gin.Context) {
// 宣告接收的變數
var data FileData
// 將request的body中的資料,自動按照json格式決議到結構體
if err := c.ShouldBindJSON(&data); err != nil {
// 回傳錯誤資訊
// gin.H封裝了生成json資料的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName)
//檔案讀取任務是將檔案內容讀取到記憶體中,
info, err := ioutil.ReadFile(path)
fmt.Print(path)
if err != nil {
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "讀取檔案內容失敗"})
fmt.Print(err.Error())
return
}
fmt.Println(info)
result := string(info)
//回傳結果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "讀取內容成功", "filename": data.FileName, "content": result})
return
}
洗掉檔案
/**洗掉檔案**/
func FilerDelete(c *gin.Context) {
// 宣告接收的變數
var data FileData
// 將request的body中的資料,自動按照json格式決議到結構體
if err := c.ShouldBindJSON(&data); err != nil {
// 回傳錯誤資訊
// gin.H封裝了生成json資料的工具
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": err.Error()})
return
}
path := fmt.Sprintf("static/txtfile/%s.txt", data.FileName) //源檔案路徑
//洗掉檔案
cuowu := os.Remove(path)
fmt.Print(path)
if cuowu != nil {
//如果洗掉失敗則輸出 file remove Error!
fmt.Println("file remove Error!")
//輸出錯誤詳細資訊
fmt.Printf("%s", cuowu)
c.JSON(http.StatusMovedPermanently, gin.H{"code:": 1, "msg: ": "洗掉檔案失敗"})
return
} else {
//如果洗掉成功則輸出 file remove OK!
fmt.Print("file remove OK!")
}
//回傳結果
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "洗掉檔案成功", "filename": data.FileName})
return
}
檔案上傳下載
- controller
+file.go
-uploadFile
+.....
-router
+router.go
-main.go
package controller
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// AddUploads 上傳檔案
func AddUploads(c *gin.Context) {
username := c.PostForm("username")
// 單個檔案
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
// dst := fmt.Sprintf("D:/桌面/檔案/updateFile/%s", username+"-"+file.Filename)
dst := fmt.Sprintf(".updateFile/%s", username+"-"+file.Filename)
// 上傳檔案到指定的目錄
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"username": username,
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
}
// DownFile
func DownFile(c *gin.Context) {
fileName := "hjz-開題報告.docx"
filepath := "./updateFile/" + fileName
list := strings.Split(fileName, "-")
downFileName := list[1]
c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downFileName))
c.Writer.Header().Add("Content-Type", "application/octet-stream")
fmt.Println(filepath)
c.File(filepath)
}
基本問題:
類型轉換
string -- int64
func DeletedFileOne(c *gin.Context) {
fidStr := c.Param("fid")
fid, err := strconv.ParseInt(fidStr, 10, 64)
if err != nil {
ResponseError(c, CodeInvalidParam)
return
}
if err2 := logic.DeletedFileOne(fid); err2 != nil {
zap.L().Error("logic.DeletedFileOne () failed ", zap.Error(err))
ResponseError(c, CodeServerBusy)
return
}
ResponseSuccess(c, CodeSuccess)
}
int64--string
strconv.FormatInt(v.NucleicAcidID, 10)
得到當前時間戳
// GetNowTime 得到現在時間的年月日的時間戳
func GetNowTime() int64 {
tm := time.Now().Format("2006-01-02")
tt, _ := time.ParseInLocation("2006-01-02", tm, time.Local)
return tt.Unix()
}
時間戳---time
time.Unix(v.TodayTime, 0)
// 秒級時間戳轉time
func UnixSecondToTime(second int64) time.Time {
return time.Unix(second, 0)
}
// 毫秒級時間戳轉time
func UnixMilliToTime(milli int64) time.Time {
return time.Unix(milli/1000, (milli%1000)*(1000*1000))
}
// 納秒級時間戳轉time
func UnixNanoToTime(nano int64) time.Time {
return time.Unix(nano/(1000*1000*1000), nano%(1000*1000*1000))
}
Gin框架決議
路由決議
中間件決議
微信小程式
uni.login({
provider: "weixin",
success: function (res) {
uni.request({
method:'POST',
url: 'http://106.15.65.147:8081/api/v1/wx/openid', //僅為示例,并非真實介面地址,
data: {
"app_id": "wx8d36d8370b6e82f0",
"code": res.code,
"method": "get",
"secret": "092d4b45d6b6c8d2b99bf82c6e23657e",
"url": "https://api.weixin.qq.com/sns/jscode2session"
},
header: {
'content-type': 'application/json' //自定義請求頭資訊
},
success: (res) => {
var result = res.data
uni.setStorageSync('open_id',result.data.open_id)
uni.setStorageSync('session_key',result.data.session_key)
}
});
},
});
轉發請求
post
// 發送post請求
func main() {
router := gin.Default()
router.GET("/test", func(c *gin.Context) {
var body = strings.NewReader("name=test&jab=teache")
response, err := http.Post("http://localhost:8888/base/captcha","application/json; charset=utf-8", body)
if err != nil || response.StatusCode != http.StatusOK {
c.Status(http.StatusServiceUnavailable)
return
}
reader := response.Body
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
extraHeaders := map[string]string{
//"Content-Disposition": `attachment; filename="gopher.png"`,
}
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
router.Run(":8080")
}
golang發送get請求第三方資料
func main() {
router := gin.Default()
router.GET("/test", func(c *gin.Context) {
response, err := http.Get("https://baidu.com")
if err != nil || response.StatusCode != http.StatusOK {
c.Status(http.StatusServiceUnavailable)
return
}
reader := response.Body
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
extraHeaders := map[string]string{
//"Content-Disposition": `attachment; filename="gopher.png"`,
}
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
router.Run(":8080")
}
跨域:
package router
import (
"net/http"
"github.com/gin-gonic/gin"
)
func CORSMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*") //允許所有IP訪問
ctx.Writer.Header().Set("Access-Control-Max-Age", "86400")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "*")
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(200)
} else {
ctx.Next()
}
}
}
實作404頁面
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/user", func(c *gin.Context) {
//指定默認值
//http://localhost:8080/user 才會列印出來默認的值
name := c.DefaultQuery("name", "枯藤")
c.String(http.StatusOK, fmt.Sprintf("hello %s", name))2020-08-05 09:22:11 星期三
})
r.NoRoute(func(c *gin.Context) {
c.String(http.StatusNotFound, "404 not found2222")
})
r.Run()
}
JSON序列化和反序列化
package main
import (
"encoding/json"
"fmt"
"math"
)
type User struct {
UserID int64 `json:"id"`
UserName string `json:"name"`
}
// 第一層次
// 第二層次
// 第五層次
func main() {
//json序列化
user := User{
UserID: math.MaxInt64,
UserName: "hjz",
}
b, err := json.Marshal(user)
if err != nil {
print(err)
}
fmt.Println(string(b))
//使用Json的反序列化
s := `{"id":9223372036854775807,"name":"hjz"}`
var data User
err = json.Unmarshal([]byte(s), &data)
if err != nil {
fmt.Println(err)
}
fmt.Printf("userId:%d,userName:%s", user.UserID, user.UserName)
}
翻譯Gin框架的日志
-controller
+ validator.go
- models
+ params.go
package controller
//翻譯Gin框架的日志
import (
"fmt"
"reflect"
"strings"
"fileWeb/models"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
)
// 定義一個全域翻譯器T
var trans ut.Translator
// InitTrans 初始化翻譯器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎屬性,實作自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注冊一個獲取json tag的自定義方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// 為SignUpParam注冊自定義校驗方法
v.RegisterStructValidation(SignUpParamStructLevelValidation, models.ParamSignUp{})
zhT := zh.New() // 中文翻譯器
enT := en.New() // 英文翻譯器
// 第一個引數是備用(fallback)的語言環境
// 后面的引數是應該支持的語言環境(支持多個)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取決于 http 請求頭的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 傳入多個locale進行查找
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}
// 注冊翻譯器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
// RemoveTopStruct 去除提示資訊中的結構體名稱
func RemoveTopStruct(fields map[string]string) map[string]string {
res := map[string]string{}
for field, err := range fields {
res[field[strings.Index(field, ".")+1:]] = err
}
return res
}
// SignUpParamStructLevelValidation 自定義SignUpParam結構體校驗函式
func SignUpParamStructLevelValidation(sl validator.StructLevel) {
su := sl.Current().Interface().(models.ParamSignUp)
if su.Password != su.RePassword {
// 輸出錯誤提示資訊,最后一個引數就是傳遞的param
sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
}
}
// ParamSignUp 注冊請求引數
type ParamSignUp struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required"`
Phone string `json:"phone" binding:"required"`
Name string `json:"name" binding:"required"`
}
雪花演算法
-pkg
- snowflake
+ snowflake.go
package snowflake
//雪花演算法
import (
"time"
"github.com/bwmarrin/snowflake"
)
var node *snowflake.Node
func Init(startTime string, machineID int64) (err error) {
var st time.Time
//指定時間因子-startTime
st, err = time.Parse("2006-01-02", startTime)
if err != nil {
return
}
snowflake.Epoch = st.UnixNano() / 1000000
node, err = snowflake.NewNode(machineID)
return
}
func GenID() int64 {
return node.Generate().Int64()
}
jwtToken
- pkg
-jwt
+ jwt.go
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v4"
)
// token的過期時間
const TokenExpireDuration = time.Hour * 2
// token的sercet用于簽名的字串
var CustomSecret []byte = []byte("疫情小程式簽名")
type CustomClaims struct {
jwt.RegisteredClaims // 內嵌標準的宣告
UserID int64 `json:"user_id"`
Username string `json:"username"`
}
// GenToken 生成JWT
func GenToken(userID int64, username string) (string, error) {
// 創建一個我們自己的宣告
claims := CustomClaims{
UserID: userID,
Username: username, // 自定義欄位
}
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(TokenExpireDuration))
claims.Issuer = "my-project"
// 使用指定的簽名方法創建簽名物件
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 使用指定的secret簽名并獲得完整的編碼后的字串token
return token.SignedString(CustomSecret)
}
// ParseToken 決議JWT
func ParseToken(tokenString string) (*CustomClaims, error) {
// 決議token
// 如果是自定義Claim結構體則需要使用 ParseWithClaims 方法
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, err error) {
// 直接使用標準的Claim則可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return CustomSecret, nil
})
if err != nil {
return nil, err
}
// 對token物件中的Claim進行型別斷言
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { // 校驗token
return claims, nil
}
return nil, errors.New("invalid token")
}
Code 回傳的錯誤
- controller
+ Code.go
package controller
// ResCode 定義回傳值型別
type ResCode int64
const (
CodeSuccess ResCode = 1000 + iota
CodeInvalidParam
CodeUserExist
CodeUserNotExist
CodeInvalidPassword
CodeServerBusy
CodeNeedLogin
CodeInvalidToken
)
var codeMsgMap = map[ResCode]string{
CodeSuccess: "success",
CodeInvalidParam: "請求引數錯誤",
CodeUserExist: "用戶名已存在",
CodeUserNotExist: "用戶名不存在",
CodeInvalidPassword: "用戶名或密碼錯誤",
CodeServerBusy: "服務器繁忙",
CodeNeedLogin: "需要登錄",
CodeInvalidToken: "無效的token",
}
// GetMsg 得到對應的錯誤
func (r ResCode) GetMsg() string {
msg, ok := codeMsgMap[r]
if !ok {
msg = codeMsgMap[CodeServerBusy]
}
return msg
}
Response 回傳回應方法
- controller
+ resopnse.go
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
)
/*
{
"code":1001,//程式中的錯誤碼
"msg":xx,提示資訊
"data":{},//資料
}
*/
type Response struct {
Code ResCode `json:"code"`
Msg interface{} `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
// ResponseError 回傳錯誤型別
func ResponseError(c *gin.Context, code ResCode) {
resp := &Response{
Code: code,
Msg: code.GetMsg(),
Data: nil,
}
c.JSON(http.StatusOK, resp)
}
// ResponseSuccess 回傳請求成功
func ResponseSuccess(c *gin.Context, data interface{}) {
resp := &Response{
Code: CodeSuccess,
Msg: CodeSuccess.GetMsg(),
Data: data,
}
c.JSON(http.StatusOK, resp)
}
// 自定義的回傳錯誤
func ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) {
resp := &Response{
Code: code,
Msg: msg,
Data: nil,
}
c.JSON(http.StatusOK, resp)
}
// ResponseSuccessLayUi 回傳Layui 資料
func ResponseSuccessLayUi(c *gin.Context, code int, msg string, count int, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"code": code,
"count": count,
"data": data,
"msg": msg,
})
}
Request 決議請求操作
- controller
+ request.go
package controller
import (
"errors"
"strconv"
"github.com/gin-gonic/gin"
)
const CtxUserIDkey = "userID"
var ErrorUserNotLogin = errors.New("用戶未登錄")
// GetCyrrentUserID 獲取當前用戶的ID
func GetCyrrentUserID(c *gin.Context) (userID int64, err error) {
uid, ok := c.Get(CtxUserIDkey)
if !ok {
err = ErrorUserNotLogin
return
}
userID, ok = uid.(int64)
if !ok {
err = ErrorUserNotLogin
return
}
return
}
// GetPageInfo 處理分頁請求的引數
func GetPageInfo(c *gin.Context) (page, page_size int64, err error) {
//獲得分頁引數offer和limit
pageStr := c.Query("page")
page_sizeStr := c.Query("size")
page, err = strconv.ParseInt(pageStr, 10, 32)
if err != nil {
page = 1
}
page_size, err = strconv.ParseInt(page_sizeStr, 10, 32)
if err != nil {
page_size = 10
}
return
}
基本請求
1.引數處理
2.業務邏輯
3.回傳資料
import (
"errors"
"strconv"
"webGin/dao/mysql"
"webGin/logic"
"webGin/models"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10" //注意這條正確
"go.uber.org/zap"
)
// SingUpHandlerInstructor 輔導員注冊
func SingUpHandlerInstructor(c *gin.Context) {
p := new(models.ParamSignUp)
if err := c.ShouldBindJSON(p); err != nil {
zap.L().Error("SingUpHandlerInstructor with invalid param ", zap.Error(err))
//判斷是否是校驗錯誤
errs, ok := err.(validator.ValidationErrors)
if !ok {
ResponseError(c, CodeInvalidParam)
return
} else {
ResponseErrorWithMsg(c, CodeInvalidParam, RemoveTopStruct(errs.Translate(trans)))
return
}
}
//業務處理
if err := logic.SingUp(p); err != nil {
zap.L().Error("logic.SingUp() failed", zap.Error(err))
//依據錯誤的型別進行回傳
if errors.Is(err, mysql.ErrorUserExist) {
ResponseError(c, CodeUserExist)
return
} else {
ResponseError(c, CodeServerBusy)
return
}
}
ResponseSuccess(c, CodeSuccess)
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550304.html
標籤:其他
上一篇:Disruptor-原始碼解讀