Gin
是一個(gè)用Go語言編寫的web框架。它是一個(gè)類似于martini
但擁有更好性能的API框架, 由于使用了httprouter
螺句,速度提高了近40倍。 如果你是性能和高效的追求者, 你會(huì)愛上Gin
。
Gin框架介紹
Go世界里最流行的Web框架糕档,Github上有32K+
star。 基于httprouter開發(fā)的Web框架愧杯。 中文文檔齊全瑞侮,簡(jiǎn)單易用的輕量級(jí)框架。
Gin框架安裝與使用
安裝
下載并安裝Gin
:
go get -u github.com/gin-gonic/gin
第一個(gè)Gin示例:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 創(chuàng)建一個(gè)默認(rèn)的路由引擎
r := gin.Default()
// GET:請(qǐng)求方式溪食;/hello:請(qǐng)求的路徑
// 當(dāng)客戶端以GET方法請(qǐng)求/hello路徑時(shí)囊卜,會(huì)執(zhí)行后面的匿名函數(shù)
r.GET("/hello", func(c *gin.Context) {
// c.JSON:返回JSON格式的數(shù)據(jù)
c.JSON(200, gin.H{
"message": "Hello world!",
})
})
// 啟動(dòng)HTTP服務(wù),默認(rèn)在0.0.0.0:8080啟動(dòng)服務(wù)
r.Run()
}
將上面的代碼保存并編譯執(zhí)行错沃,然后使用瀏覽器打開127.0.0.1:8080/hello
就能看到一串JSON字符串栅组。
RESTful API
REST與技術(shù)無關(guān),代表的是一種軟件架構(gòu)風(fēng)格枢析,REST是Representational State Transfer的簡(jiǎn)稱玉掸,中文翻譯為“表征狀態(tài)轉(zhuǎn)移”或“表現(xiàn)層狀態(tài)轉(zhuǎn)化”。
簡(jiǎn)單來說醒叁,REST的含義就是客戶端與Web服務(wù)器之間進(jìn)行交互的時(shí)候司浪,使用HTTP協(xié)議中的4個(gè)請(qǐng)求方法代表不同的動(dòng)作泊业。
-
GET
用來獲取資源 -
POST
用來新建資源 -
PUT
用來更新資源 -
DELETE
用來刪除資源。
只要API程序遵循了REST風(fēng)格啊易,那就可以稱其為RESTful API脱吱。目前在前后端分離的架構(gòu)中,前后端基本都是通過RESTful API來進(jìn)行交互认罩。
例如箱蝠,我們現(xiàn)在要編寫一個(gè)管理書籍的系統(tǒng),我們可以查詢對(duì)一本書進(jìn)行查詢垦垂、創(chuàng)建宦搬、更新和刪除等操作,我們?cè)诰帉懗绦虻臅r(shí)候就要設(shè)計(jì)客戶端瀏覽器與我們Web服務(wù)端交互的方式和路徑劫拗。按照經(jīng)驗(yàn)我們通常會(huì)設(shè)計(jì)成如下模式:
請(qǐng)求方法 | URL | 含義 |
---|---|---|
GET | /book | 查詢書籍信息 |
POST | /create_book | 創(chuàng)建書籍記錄 |
POST | /update_book | 更新書籍信息 |
POST | /delete_book | 刪除書籍信息 |
同樣的需求我們按照RESTful API設(shè)計(jì)如下:
請(qǐng)求方法 | URL | 含義 |
---|---|---|
GET | /book | 查詢書籍信息 |
POST | /book | 創(chuàng)建書籍記錄 |
PUT | /book | 更新書籍信息 |
DELETE | /book | 刪除書籍信息 |
Gin框架支持開發(fā)RESTful API的開發(fā)间校。
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",
})
})
}
開發(fā)RESTful API的時(shí)候我們通常使用Postman來作為客戶端的測(cè)試工具。
Gin渲染
HTML渲染
我們首先定義一個(gè)存放模板文件的templates
文件夾页慷,然后在其內(nèi)部按照業(yè)務(wù)分別定義一個(gè)posts
文件夾和一個(gè)users
文件夾憔足。 posts/index.html
文件的內(nèi)容如下:
{{define "posts/index.html"}}
<!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>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}
users/index.html
文件的內(nèi)容如下:
{{define "users/index.html"}}
<!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>users/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}
Gin框架中使用LoadHTMLGlob()
或者LoadHTMLFiles()
方法進(jìn)行HTML模板渲染。
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")
//r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "posts/index",
})
})
r.GET("users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "users/index",
})
})
r.Run(":8080")
}
自定義模板函數(shù)
定義一個(gè)不轉(zhuǎn)義相應(yīng)內(nèi)容的safe
模板函數(shù)如下:
func main() {
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML{
return template.HTML(str)
},
})
router.LoadHTMLFiles("./index.tmpl")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", "<a )
})
router.Run(":8080")
}
在index.tmpl
中使用定義好的safe
模板函數(shù):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>修改模板引擎的標(biāo)識(shí)符</title>
</head>
<body>
<div>{{ . | safe }}</div>
</body>
</html>
靜態(tài)文件處理
當(dāng)我們渲染的HTML文件中引用了靜態(tài)文件時(shí)酒繁,我們只需要按照以下方式在渲染頁面前調(diào)用gin.Static
方法即可滓彰。
func main() {
r := gin.Default()
r.Static("/static", "./static")
r.LoadHTMLGlob("templates/**/*")
// ...
r.Run(":8080")
}
使用模板繼承
Gin框架默認(rèn)都是使用單模板,如果需要使用block template
功能州袒,可以通過"github.com/gin-contrib/multitemplate"
庫(kù)實(shí)現(xiàn)揭绑,具體示例如下:
首先,假設(shè)我們項(xiàng)目目錄下的templates文件夾下有以下模板文件郎哭,其中home.tmpl
和index.tmpl
繼承了base.tmpl
:
templates
├── includes
│ ├── home.tmpl
│ └── index.tmpl
├── layouts
│ └── base.tmpl
└── scripts.tmpl
然后我們定義一個(gè)loadTemplates
函數(shù)如下:
func loadTemplates(templatesDir string) multitemplate.Renderer {
r := multitemplate.NewRenderer()
layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
if err != nil {
panic(err.Error())
}
includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
if err != nil {
panic(err.Error())
}
// 為layouts/和includes/目錄生成 templates map
for _, include := range includes {
layoutCopy := make([]string, len(layouts))
copy(layoutCopy, layouts)
files := append(layoutCopy, include)
r.AddFromFiles(filepath.Base(include), files...)
}
return r
}
我們?cè)?code>main函數(shù)中
func indexFunc(c *gin.Context){
c.HTML(http.StatusOK, "index.tmpl", nil)
}
func homeFunc(c *gin.Context){
c.HTML(http.StatusOK, "home.tmpl", nil)
}
func main(){
r := gin.Default()
r.HTMLRender = loadTemplates("./templates")
r.GET("/index", indexFunc)
r.GET("/home", homeFunc)
r.Run()
}
補(bǔ)充文件路徑處理
關(guān)于模板文件和靜態(tài)文件的路徑他匪,我們需要根據(jù)公司/項(xiàng)目的要求進(jìn)行設(shè)置】溲校可以使用下面的函數(shù)獲取當(dāng)前執(zhí)行程序的路徑邦蜜。
func getCurrentPath() string {
if ex, err := os.Executable(); err == nil {
return filepath.Dir(ex)
}
return "./"
}
JSON渲染
func main() {
r := gin.Default()
// gin.H 是map[string]interface{}的縮寫
r.GET("/someJSON", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
})
r.GET("/moreJSON", func(c *gin.Context) {
// 方法二:使用結(jié)構(gòu)體
var msg struct {
Name string `json:"user"`
Message string
Age int
}
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.JSON(http.StatusOK, msg)
})
r.Run(":8080")
}
XML渲染
注意需要使用具名的結(jié)構(gòu)體類型。
func main() {
r := gin.Default()
// gin.H 是map[string]interface{}的縮寫
r.GET("/someXML", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
})
r.GET("/moreXML", func(c *gin.Context) {
// 方法二:使用結(jié)構(gòu)體
type MessageRecord struct {
Name string
Message string
Age int
}
var msg MessageRecord
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.XML(http.StatusOK, msg)
})
r.Run(":8080")
}
YMAL渲染
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
})
protobuf渲染
r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// protobuf 的具體定義寫在 testdata/protoexample 文件中亥至。
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// 請(qǐng)注意悼沈,數(shù)據(jù)在響應(yīng)中變?yōu)槎M(jìn)制數(shù)據(jù)
// 將輸出被 protoexample.Test protobuf 序列化了的數(shù)據(jù)
c.ProtoBuf(http.StatusOK, data)
})
獲取參數(shù)
獲取querystring參數(shù)
querystring
指的是URL中?
后面攜帶的參數(shù),例如:/user/search?username=小王子&address=沙河
抬闯。 獲取請(qǐng)求的querystring參數(shù)的方法如下:
func main() {
//Default返回一個(gè)默認(rèn)的路由引擎
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
username := c.DefaultQuery("username", "小王子")
//username := c.Query("username")
address := c.Query("address")
//輸出json結(jié)果給調(diào)用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run()
}
獲取form參數(shù)
請(qǐng)求的數(shù)據(jù)通過form表單來提交井辆,例如向/user/search
發(fā)送一個(gè)POST請(qǐng)求,獲取請(qǐng)求數(shù)據(jù)的方式如下:
func main() {
//Default返回一個(gè)默認(rèn)的路由引擎
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
// DefaultPostForm取不到值時(shí)會(huì)返回指定的默認(rèn)值
//username := c.DefaultPostForm("username", "小王子")
username := c.PostForm("username")
address := c.PostForm("address")
//輸出json結(jié)果給調(diào)用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
獲取path參數(shù)
請(qǐng)求的參數(shù)通過URL路徑傳遞溶握,例如:/user/search/小王子/沙河
杯缺。 獲取請(qǐng)求URL路徑中的參數(shù)的方式如下。
func main() {
//Default返回一個(gè)默認(rèn)的路由引擎
r := gin.Default()
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
//輸出json結(jié)果給調(diào)用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
參數(shù)綁定
為了能夠更方便的獲取請(qǐng)求相關(guān)參數(shù)睡榆,提高開發(fā)效率萍肆,我們可以基于請(qǐng)求的Content-Type
識(shí)別請(qǐng)求數(shù)據(jù)類型并利用反射機(jī)制自動(dòng)提取請(qǐng)求中QueryString
袍榆、form表單
、JSON
塘揣、XML
等參數(shù)到結(jié)構(gòu)體中包雀。 下面的示例代碼演示了.ShouldBind()
強(qiáng)大的功能,它能夠基于請(qǐng)求自動(dòng)提取JSON
亲铡、form表單
和QueryString
類型的數(shù)據(jù)才写,并把值綁定到指定的結(jié)構(gòu)體對(duì)象。
// 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()會(huì)根據(jù)請(qǐng)求的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()會(huì)根據(jù)請(qǐng)求的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()})
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
ShouldBind
會(huì)按照下面的順序解析請(qǐng)求中的數(shù)據(jù)完成綁定:
- 如果是
GET
請(qǐng)求奖蔓,只使用Form
綁定引擎(query
)赞草。 - 如果是
POST
請(qǐng)求,首先檢查content-type
是否為JSON
或XML
吆鹤,然后再使用Form
(form-data
)厨疙。
文件上傳
單個(gè)文件上傳
文件上傳前端頁面代碼:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>上傳文件示例</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="f1">
<input type="submit" value="上傳">
</form>
</body>
</html>
后端gin框架部分代碼:
func main() {
router := gin.Default()
// 處理multipart forms提交文件時(shí)默認(rèn)的內(nèi)存限制是32 MiB
// 可以通過下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 單個(gè)文件
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
// 上傳文件到指定的目錄
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}
多個(gè)文件上傳
func main() {
router := gin.Default()
// 處理multipart forms提交文件時(shí)默認(rèn)的內(nèi)存限制是32 MiB
// 可以通過下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]
for index, file := range files {
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
// 上傳文件到指定的目錄
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
router.Run()
}
重定向
HTTP重定向
HTTP 重定向很容易。 內(nèi)部疑务、外部重定向均支持沾凄。
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})
路由重定向
路由重定向,使用HandleContext
:
r.GET("/test", func(c *gin.Context) {
// 指定重定向的URL
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"hello": "world"})
})
Gin路由
普通路由
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
此外知允,還有一個(gè)可以匹配所有請(qǐng)求方法的Any
方法如下:
r.Any("/test", func(c *gin.Context) {...})
為沒有配置處理函數(shù)的路由添加處理程序撒蟀,默認(rèn)情況下它返回404代碼,下面的代碼為沒有匹配到路由的請(qǐng)求都返回views/404.html
頁面廊镜。
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
路由組
我們可以將擁有共同URL前綴的路由劃分為一個(gè)路由組牙肝。習(xí)慣性一對(duì){}
包裹同組的路由,這只是為了看著清晰嗤朴,你用不用{}
包裹功能上沒什么區(qū)別。
func main() {
r := gin.Default()
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})
}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
}
r.Run()
}
路由組也是支持嵌套的虫溜,例如:
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
// 嵌套路由組
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {...})
}
通常我們將路由分組用在劃分業(yè)務(wù)邏輯或劃分API版本時(shí)雹姊。
路由原理
Gin框架中的路由使用的是httprouter這個(gè)庫(kù)。
其基本原理就是構(gòu)造一個(gè)路由地址的前綴樹衡楞。
Gin中間件
Gin框架允許開發(fā)者在處理請(qǐng)求的過程中吱雏,加入用戶自己的鉤子(Hook)函數(shù)。這個(gè)鉤子函數(shù)就叫中間件瘾境,中間件適合處理一些公共的業(yè)務(wù)邏輯歧杏,比如登錄認(rèn)證、權(quán)限校驗(yàn)迷守、數(shù)據(jù)分頁犬绒、記錄日志、耗時(shí)統(tǒng)計(jì)等兑凿。
定義中間件
Gin中的中間件必須是一個(gè)gin.HandlerFunc
類型凯力。例如我們像下面的代碼一樣定義一個(gè)統(tǒng)計(jì)請(qǐng)求耗時(shí)的中間件茵瘾。
// StatCost 是一個(gè)統(tǒng)計(jì)耗時(shí)請(qǐng)求耗時(shí)的中間件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子") // 可以通過c.Set在請(qǐng)求上下文中設(shè)置值,后續(xù)的處理函數(shù)能夠取到該值
// 調(diào)用該請(qǐng)求的剩余處理程序
c.Next()
// 不調(diào)用該請(qǐng)求的剩余處理程序
// c.Abort()
// 計(jì)算耗時(shí)
cost := time.Since(start)
log.Println(cost)
}
}
注冊(cè)中間件
在gin框架中咐鹤,我們可以為每個(gè)路由添加任意數(shù)量的中間件拗秘。
為全局路由注冊(cè)
func main() {
// 新建一個(gè)沒有任何默認(rèn)中間件的路由
r := gin.New()
// 注冊(cè)一個(gè)全局中間件
r.Use(StatCost())
r.GET("/test", func(c *gin.Context) {
name := c.MustGet("name").(string) // 從上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
r.Run()
}
為某個(gè)路由單獨(dú)注冊(cè)
// 給/test2路由單獨(dú)注冊(cè)中間件(可注冊(cè)多個(gè))
r.GET("/test2", StatCost(), func(c *gin.Context) {
name := c.MustGet("name").(string) // 從上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
為路由組注冊(cè)中間件
為路由組注冊(cè)中間件有以下兩種寫法。
寫法1:
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
寫法2:
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
中間件注意事項(xiàng)
gin默認(rèn)中間件
gin.Default()
默認(rèn)使用了Logger
和Recovery
中間件祈惶,其中:
-
Logger
中間件將日志寫入gin.DefaultWriter
雕旨,即使配置了GIN_MODE=release
。 -
Recovery
中間件會(huì)recover任何panic
捧请。如果有panic的話凡涩,會(huì)寫入500響應(yīng)碼。
如果不想使用上面兩個(gè)默認(rèn)的中間件血久,可以使用gin.New()
新建一個(gè)沒有任何默認(rèn)中間件的路由突照。
gin中間件中使用goroutine
當(dāng)在中間件或handler
中啟動(dòng)新的goroutine
時(shí)氧吐,不能使用原始的上下文(c *gin.Context),必須使用其只讀副本(c.Copy()
)翠拣。
運(yùn)行多個(gè)服務(wù)
我們可以在多個(gè)端口啟動(dòng)服務(wù)欣范,例如:
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或者自行開啟兩個(gè)goroutine分別啟動(dòng)兩個(gè)服務(wù)
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
gin路由
- 基本路由
gin采用的路由庫(kù)基于httprouter做的变泄。
-
Restful風(fēng)格的API
支持Restful風(fēng)格的API
-
是一種互聯(lián)網(wǎng)應(yīng)用程序的API設(shè)計(jì)理念,URL定位資源恼琼,用HTTP描述操作妨蛹。
獲取文章 /blog/getXxx Get blog/Xxx
添加 /blog/addXxx POST /blog/Xxx
修改 /blog/updateXxx PUT /blog/Xxx
添加 /blog/delXxx DELETE /blog/Xxx
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1、創(chuàng)建路由
r := gin.Default()
//2晴竞、綁定路由規(guī)則蛙卤,執(zhí)行函數(shù)
//gin.Context,封裝了request和response
r.GET("/",func(c *gin.Context){
c.String(http.StatusOK,"hello world!")
})
//3、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
- API參數(shù)
- 可以通過Context的Param方法來獲取API參數(shù)
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1颓鲜、創(chuàng)建路由
r := gin.Default()
//2表窘、綁定路由規(guī)則典予,執(zhí)行函數(shù)
//gin.Context,封裝了request和response
r.GET("/user/:name/*action",func(c *gin.Context){
name := c.Param("name")
action := c.Param("action")
c.String(http.StatusOK,name+" is "+action)
})
r.POST("/xxxPost",getting)
r.PUT("/xxxPut")
//3、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
func getting(c *gin.Context){
}
- URL參數(shù)
- URL 參數(shù)可以通過DefaultQuery()或Query()方法獲取乐严。
- DefaultQuery()若參數(shù)不對(duì)瘤袖,則返回默認(rèn)值,Query()若不存在昂验,返回空串捂敌。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1、創(chuàng)建路由
r := gin.Default()
//2既琴、綁定路由規(guī)則占婉,執(zhí)行函數(shù)
//gin.Context,封裝了request和response
r.GET("/welcom",func(c *gin.Context){
//第二個(gè)參數(shù)默認(rèn)值
name := c.DefaultQuery("name","jack")
c.String(http.StatusOK,fmt.Sprintf("hello %s",name))
})
//3、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
- 表單參數(shù)
- 表單傳輸為post請(qǐng)求甫恩,http常見的傳輸格式4種:
- application/json
- application/x-www-form-urlencoded
- application/xml
- multipart/form-data
- 表單參數(shù)可以通過PostForm()方法獲取逆济,該方法默認(rèn)解析的是x-www-form-urlencoded或from-data格式的參數(shù)
后臺(tái):
- 表單傳輸為post請(qǐng)求甫恩,http常見的傳輸格式4種:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1、創(chuàng)建路由
r := gin.Default()
//2磺箕、綁定路由規(guī)則奖慌,執(zhí)行函數(shù)
//gin.Context,封裝了request和response
r.POST("/form",func(c *gin.Context){
//表單參數(shù)設(shè)置默認(rèn)值
type1 := c.DefaultPostForm("type","alert")
//接收其他的
username := c.PostForm("username")
password := c.PostForm("password")
//多選框
hobbys:= c.PostFormArray("hobby")
c.String(http.StatusOK,fmt.Sprintf("type is %s,username is %s,password is %s,hobby is %v",type1,username,password,hobbys))
//第二個(gè)參數(shù)默認(rèn)值
name := c.DefaultQuery("name","jack")
c.String(http.StatusOK,fmt.Sprintf("hello %s",name))
})
//3、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<form action="http://127.0.0.1:8000/form" method="post" enctype="application/x-www-form-urlencoded">
用戶名:<input type="text" name="username">
<br>
密  碼:<input type="password" name="password">
興  趣:
<input type="checkbox" value="run" name="hobby"> 跑步
<input type="checkbox" value="game" name="hobby"> 游戲
<input type="checkbox" value="money" name="hobby"> 金錢
<br>
<input type="submit" value="登錄">
</form>
</body>
</html>
- 上傳單個(gè)文件
- multipart/form-data格式用于文件上傳
- gin文件上傳與原生的net/http方法類似松靡,不同在于gin把原生的request封裝到c.Request中
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
//gin的helloworld
func main(){
//1简僧、創(chuàng)建路由
r := gin.Default()
//2、綁定路由規(guī)則雕欺,執(zhí)行函數(shù)
//gin.Context,封裝了request和response
r.POST("/upload",func(c *gin.Context){
//表單取文件
file,_ := c.FormFile("file")
fmt.Println(file.Filename)
//傳到項(xiàng)目根目錄岛马,名字就用本身的
c.SaveUploadedFile(file,file.Filename)
//打印信息
c.String(200,fmt.Sprintf("'%s' upload",file.Filename))
})
//3、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<form action="http://127.0.0.1:8000/upload" method="post" enctype="multipart/form-data">
頭像:<input type="file" name="file">
<br>
<input type="submit" name="提交">
</form>
</body>
</html>
- 上傳多個(gè)文件
改:
<input type="file" name="files" multiple>
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//gin的helloworld
func main(){
//1屠列、創(chuàng)建路由
r := gin.Default()
//2啦逆、綁定路由規(guī)則,執(zhí)行函數(shù)
//gin.Context,封裝了request和response
//限制表單上傳文件大小 8M笛洛,默認(rèn)32M
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{
//逐個(gè)存
err := c.SaveUploadedFile(file,file.Filename);
if err != nil {
c.String(http.StatusBadRequest,fmt.Sprintf("get err %s",err.Error()))
return
}
}
c.String(200,fmt.Sprintf("upload ok %d files",len(files)))
})
//3蹦浦、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
- routes group
- routes group是為了管理相同的uml
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
//gin的helloworld
func main(){
//1、創(chuàng)建路由
//默認(rèn)使用2個(gè)中間件Logger(),Recovery()
r := gin.Default()
//路由組1撞蜂,處理GET請(qǐng)求
v1 := r.Group("/v1")
//{}書寫規(guī)范
{
v1.GET("/login",login)
v1.GET("/submit",submit)
}
v2 := r.Group("/v2")
{
v2.POST("/login",login)
v2.POST("/submit",submit)
}
//3、監(jiān)聽端口,默認(rèn)端口8080
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))
}
- 路由原理
- httproter會(huì)將所有路由規(guī)則構(gòu)造一棵前綴樹侥袜。
package main
import (
"github.com/gin-gonic/gin"
)
//gin的helloworld
func main(){
//1蝌诡、創(chuàng)建路由
//默認(rèn)使用2個(gè)中間件Logger(),Recovery()
r := gin.Default()
r.POST("/",xxx)
r.POST("search",xxx)
r.POST("support",xxx)
r.POST("/blog/:post",xxx)
r.POST("/contact",xxx)
r.POST("/about",xxx)
//3、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
gin數(shù)據(jù)解析和綁定
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//定義接收數(shù)據(jù)的結(jié)構(gòu)體
type Login struct {
//binding:”required"修飾的字段枫吧,若接收為空值浦旱,則報(bào)錯(cuò),是必須字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main(){
//1九杂、創(chuàng)建路由
//默認(rèn)使用2個(gè)中間件Logger(),Recovery()
r := gin.Default()
//r.POST("/",xxx)
//r.POST("search",xxx)
//r.POST("support",xxx)
//r.POST("/blog/:post",xxx)
//r.POST("/contact",xxx)
//r.POST("/about",xxx)
//json綁定
r.POST("loginJSON",func(c *gin.Context){
//聲明接收的變量
var json Login
//將request的body中的數(shù)據(jù)颁湖,自動(dòng)按照J(rèn)SON格式解析到結(jié)構(gòu)體
if err := c.ShouldBindJSON(&json); err != nil{
//返回錯(cuò)誤信息
//gin.H封裝了生成JSON數(shù)據(jù)的工具
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
//判斷用戶名密碼是否正確
if json.User != "root" || json.Password != "admin"{
c.JSON(http.StatusBadRequest,gin.H{"status":"304"})
return
}
c.JSON(http.StatusOK,gin.H{"status":"200"})
})
//3宣蠕、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
表單數(shù)據(jù)解析和綁定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<form action="http://127.0.0.1:8000/loginForm" method="post" enctype="multipart/form-data">
用戶名:<input type="text" name="username">
<br>
密  碼:<input type="password" name="password">
<input type="submit" value="登錄">
</form>
</body>
</html>
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//定義接收數(shù)據(jù)的結(jié)構(gòu)體
type Login struct {
//binding:”required"修飾的字段,若接收為空值甥捺,則報(bào)錯(cuò)抢蚀,是必須字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main(){
//1窜醉、創(chuàng)建路由
//默認(rèn)使用2個(gè)中間件Logger(),Recovery()
r := gin.Default()
//json綁定
r.POST("/loginForm",func(c *gin.Context){
//聲明接收的變量
var form Login
//bind()默認(rèn)解析并綁定form格式
//根據(jù)請(qǐng)求頭中content-type自動(dòng)推斷
if err := c.Bind(&form);err != nil{
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
//判斷用戶名密碼是否正確
if form.User != "root" || form.Password != "admin"{
c.JSON(http.StatusBadRequest,gin.H{"status":"304"})
return
}
c.JSON(http.StatusOK,gin.H{"status":"200"})
})
//3脑蠕、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
URL數(shù)據(jù)解析和綁定
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
//定義接收數(shù)據(jù)的結(jié)構(gòu)體
type Login struct {
//binding:”required"修飾的字段,若接收為空值粤铭,則報(bào)錯(cuò)吴侦,是必須字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main(){
//1屋休、創(chuàng)建路由
//默認(rèn)使用2個(gè)中間件Logger(),Recovery()
r := gin.Default()
//json綁定
r.GET("/:user/:password",func(c *gin.Context){
//聲明接收的變量
var login Login
//bind()默認(rèn)解析并綁定form格式
//根據(jù)請(qǐng)求頭中content-type自動(dòng)推斷
if err := c.ShouldBindUri(&login);err != nil{
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
//判斷用戶名密碼是否正確
if login.User != "root" || login.Password != "admin"{
c.JSON(http.StatusBadRequest,gin.H{"status":"304"})
return
}
c.JSON(http.StatusOK,gin.H{"status":"200"})
})
//3、監(jiān)聽端口,默認(rèn)端口8080
r.Run(":8000")
}
各種數(shù)據(jù)格式響應(yīng)
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/testdata/protoexample"
)
//多種響應(yīng)方式
func main(){
r := gin.Default()
//1.json
r.GET("/someJSON",func(c *gin.Context){
c.JSON(200,gin.H{"message":"someJSON","status":200})
})
//2.結(jié)構(gòu)體響應(yīng)
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)
})
//3.XML
r.GET("/someXML",func(c *gin.Context){
c.XML(200,gin.H{"message":"abc"})
})
//4.YAML響應(yīng)
r.GET("/someYAML",func(c *gin.Context){
c.YAML(200,gin.H{"name":"zhangsan"})
})
//5.protobuf格式备韧,谷歌開發(fā)的高效存儲(chǔ)讀取工具
//如果自己構(gòu)建一個(gè)傳輸格式劫樟,該是什么格式?
r.GET("/someProtoBuf",func(c *gin.Context){
reps := []int64{int64(1),int64(2)}
//定義數(shù)據(jù)
label := "label"
//傳protobuf格式數(shù)據(jù)
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
c.ProtoBuf(200,data)
})
r.Run(":8000")
}
HTML模板渲染
- gin支持HTML模板织堂,然后根據(jù)模板參數(shù)進(jìn)行配置并返回相應(yīng)的數(shù)據(jù)叠艳,本質(zhì)上就是字符串替換
- LoadHTMLGlob()方法可以加載模板文件
//html渲染
//加載模板文件
r.LoadHTMLGlob("../template/*")
//r.LoadHTMLFiles("template/index.tmpl")
r.GET("/index",func(c *gin.Context){
//根據(jù)文件名渲染
//最終JSON將title替換
c.HTML(200,"index.tmpl",gin.H{"title":"我的標(biāo)題"})
})
<html>
<hl>
{{.title}}
</hl>
</html>
重定向
//重定向
r.GET("/redirect",func(c *gin.Context){
c.Redirect(http.StatusMovedPermanently,"http://www.baidu.com")
})
- 同步異步
- goroutine機(jī)制可以方便的實(shí)現(xiàn)異步處理
- 另外,在啟動(dòng)新的goroutine時(shí)捧挺,不應(yīng)該使用原始上下文虑绵,必須使用它的只讀副本。
//異步
r.GET("/long_async",func(c *gin.Context){
//需要搞一個(gè)副本
copyContext := c.Copy()
go func(){
time.Sleep(3*time.Second)
log.Println("異步執(zhí)行"+ copyContext.Request.URL.Path)
}()
})
//同步
r.GET("/long_sync",func(c *gin.Context){
time.Sleep(3*time.Second)
log.Println("同步執(zhí)行"+ c.Request.URL.Path)
})
中間件
- gin中間件
- gin可以構(gòu)建中間件闽烙,但它只對(duì)注冊(cè)過的路由函數(shù)起作用翅睛。
- 對(duì)于分組路由,嵌套使用中間件黑竞,可以限定中間件的作用范圍捕发。
- 中間件分為全局中間件、單個(gè)路由中間件和群組中間件
- gin中間件必須是一個(gè)gin HandlerFunc類型很魂。
全局中間件
- 所有請(qǐng)求都經(jīng)過此中間件扎酷。
//定義中間件
func MiddleWare()gin.HandlerFunc{
return func(c *gin.Context){
t := time.Now()
fmt.Println("中間件開始執(zhí)行了。")
//設(shè)置變量到context到key中遏匆,可以通過Get取
c.Set("request","中間件")
//執(zhí)行中間件
c.Next()
status := c.Writer.Status()
fmt.Println("中間件執(zhí)行完畢",status)
t2 := time.Since(t)
fmt.Println("time:",t2)
}
}
//注冊(cè)中間件
r.Use(MiddleWare())
//代碼規(guī)范
{
r.GET("/middleware",func(c *gin.Context){
//取值
req,_ := c.Get("request")
fmt.Println("request",req)
//頁面接收
c.JSON(200,gin.H{"request":req})
})
}
- Next()方法
- 看源碼
- 局部中間件
//注冊(cè)中間件
r.Use(MiddleWare())
//代碼規(guī)范
{
r.GET("/middleware",func(c *gin.Context){
//取值
req,_ := c.Get("request")
fmt.Println("request",req)
//頁面接收
c.JSON(200,gin.H{"request":req})
})
//根路由后面是定義的局部中間件
r.GET("/middleware2",MiddleWare(),func(c *gin.Context){
//取值
req,_ := c.Get("request")
fmt.Println("request2",req)
//頁面接收
c.JSON(200,gin.H{"request":req})
})
}
- 中間件練習(xí)
- 定義一個(gè)程序計(jì)時(shí)中間件法挨,然后定義2個(gè)路由,執(zhí)行函數(shù)后應(yīng)該打印統(tǒng)計(jì)的執(zhí)行時(shí)間
//定義計(jì)時(shí)中間件
func MiddleTime(c *gin.Context){
start:= time.Now()
//執(zhí)行中間件
c.Next()
since := time.Since(start)
fmt.Println("程序用時(shí):",since)
}
//另一種形式
r.Use(MiddleTime)
shoppingGroup := r.Group("/shopping")
{
shoppingGroup.GET("/index",shopIndexHandler)
shoppingGroup.GET("/home",shopHomeHandle)
}
func shopHomeHandle(c *gin.Context){
time.Sleep(3*time.Second)
}
func shopIndexHandler(c *gin.Context){
time.Sleep(5*time.Second)
}
- Cookie:
- HTTP是無狀態(tài)協(xié)議幅聘,服務(wù)器不能記錄瀏覽器的訪問狀態(tài)凡纳,也就是說服務(wù)器不能區(qū)分兩次請(qǐng)求是否同一個(gè)客戶端發(fā)出。
- cookie解決http協(xié)議無狀態(tài)的方案之一帝蒿,
- cookie實(shí)際就是服務(wù)器保存在瀏覽器上的一段信息荐糜,瀏覽器有了cookie之后,每次向服務(wù)器發(fā)送請(qǐng)求時(shí)都會(huì)同時(shí)將信息發(fā)送給服務(wù)器,服務(wù)器收到請(qǐng)求后暴氏,就可以根據(jù)該信息處理請(qǐng)求延塑。
- cookie由服務(wù)器創(chuàng)建,并發(fā)送給瀏覽器答渔,最終由瀏覽器保存关带。
cookie的用途:
- 保持用戶登錄狀態(tài)
- 京東購(gòu)物車就是這樣用的。
cookie的使用:
- 測(cè)試服務(wù)端發(fā)送cookie給客戶端研儒,客戶端請(qǐng)求時(shí)攜帶cookie豫缨。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main(){
r := gin.Default()
r.GET("cookie",func(c *gin.Context){
//獲取客戶端是否攜帶cookie
cookie,err := c.Cookie("key_cookie")
if err != nil{
cookie = "NotSet"
//設(shè)置cookie 時(shí)間單位s,path.cookie所在目錄,secure是否通過https訪問,httpOnly bool是否允許別人獲取自己的cookie
c.SetCookie("key_cookie","value_cookie",
60,"/","localhost",false,true)
}
fmt.Printf("cookie的值是:%s\n",cookie)
})
r.Run(":8000")
}
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
//設(shè)置登錄權(quán)限
func MiddleLogin(c *gin.Context){
//獲取客戶端cookie并校驗(yàn)
cookie,err := c.Cookie("abc")
if err == nil{
if cookie == "123"{
c.Next()
return
}
}
//返回錯(cuò)誤
c.JSON(http.StatusUnauthorized,gin.H{"error":"err"})
c.Abort()//若驗(yàn)證不通過端朵,不再執(zhí)行之后的信息好芭。
return
}
func main(){
r := gin.Default()
r.GET("/login",func(c *gin.Context){
c.SetCookie("abc","123",60,"/","localhost",false,true)
c.String(200,"login success!")
})
r.GET("/home",MiddleLogin,func(c *gin.Context){
c.JSON(200,gin.H{"data":"home"})
})
r.Run(":8000")
}
- Cookie缺點(diǎn):
- 不安全
- 增加帶寬消耗
- 可以被禁用
- cookie有上限
- session:可以彌補(bǔ)cookie的不足,必須依賴于cookie才能使用冲呢,生成一個(gè)sessionid放在cookie里傳給客戶端就可以舍败。
- session中間件開發(fā)
- 設(shè)計(jì)一個(gè)通用的session服務(wù),支持內(nèi)存存儲(chǔ)和redis存儲(chǔ)敬拓。
- session模塊設(shè)計(jì)
- 本質(zhì)上就是一個(gè)k-v系統(tǒng)邻薯,通過key進(jìn)行增刪改查
- session可以存儲(chǔ)在內(nèi)存或redis(2個(gè)版本)
- session接口設(shè)計(jì):
- Set()
- Get()
- Del()
- Save():Session存儲(chǔ),redis延遲加載(用的時(shí)候再加載)
- SessionMgr接口設(shè)計(jì):
- Init():初始化乘凸,加載redis地址
- CreateSession():創(chuàng)建一個(gè)新的session
- GetSession(): 通過sessionID獲取對(duì)應(yīng)的session對(duì)象value
- MemorySession設(shè)計(jì):
- 定義MemorySession對(duì)象(字段:sessionid厕诡,存kv的map,讀寫鎖)
- 構(gòu)造函數(shù):為了獲取對(duì)象
- Set()
- Get()
- Del()
- Save()
- MemorySessionMgr設(shè)計(jì):
- 定義MemorySessionMgr對(duì)象(字段:存放所有session的map营勤,讀寫鎖)
- 構(gòu)造函數(shù)
- Init():初始化灵嫌,加載redis地址
- CreateSession():創(chuàng)建一個(gè)新的session
- GetSession(): 通過sessionID獲取對(duì)應(yīng)的session對(duì)象value
- RedisSession設(shè)計(jì):
- 定義RedisSession對(duì)象(字段:sessionid,存kv的map葛作,讀寫鎖寿羞,redis連接池,記錄內(nèi)存中map是否被修改的標(biāo)記)
- 構(gòu)造函數(shù):為了獲取對(duì)象
- Set():將session存到內(nèi)存中的map
- Get():取數(shù)據(jù)赂蠢,實(shí)現(xiàn)延遲加載
- Del()
- Save():將session存到redis
- RedisSessionMgr設(shè)計(jì):
- 定義RedisSessionMgr對(duì)象(字段:redis地址绪穆,redis密碼,連接池虱岂,讀寫鎖玖院,大map)
- 構(gòu)造函數(shù)
- Init():初始化,加載redis地址
- CreateSession():創(chuàng)建一個(gè)新的session
- GetSession(): 通過sessionID獲取對(duì)應(yīng)的session對(duì)象value
session.go
package session
type Session interface {
Set(key string,value interface{})error
Get(key string)(interface{},error)
Del(key string)error
Save()error
}
session_mgr.go
package session
//定義管理者第岖,管理所有session
type SessionMgr interface {
//初始化
Init(addr string,options ...string)(err error)
CreateSession(session Session,err error)
Get(sessionId string)(session Session,err error)
}
memory.go
package session
import (
"sync"
"errors"
)
//定義MemorySession對(duì)象(字段:sessionid司恳,存kv的map,讀寫鎖)
//- 構(gòu)造函數(shù):為了獲取對(duì)象
//- Set()
//- Get()
//- Del()
//- Save()
//對(duì)象
type MemorySession struct {
sessionId string
//存kv
data map[string]interface{}
rwlock sync.RWMutex
}
//構(gòu)造函數(shù)
func NewMemorySession(id string) *MemorySession{
s := &MemorySession{
sessionId: id,
data: make(map[string]interface{},16),
}
return s
}
func (m *MemorySession)Set(key string,value interface{})(err error){
//加鎖
m.rwlock.Lock()
defer m.rwlock.Unlock()
//設(shè)置值
m.data[key] = value
return
}
func (m *MemorySession)Get(key string)(value interface{},err error){
//加鎖
m.rwlock.Lock()
defer m.rwlock.Unlock()
value,ok := m.data[key]
if !ok {
err = errors.New("key not exists in session")
return
}
return
}
func (m *MemorySession)Del(key string)(err error){
//加鎖
m.rwlock.Lock()
defer m.rwlock.Unlock()
delete(m.data,key)
return
}
func (m *MemorySession)Save()(err error){
return
}
redis_session.go
package session
import (
"encoding/json"
"github.com/gomodule/redigo/redis"
"errors"
"sync"
)
//定義RedisSession對(duì)象(字段:sessionid绍傲,存kv的map,讀寫鎖)
//- 構(gòu)造函數(shù):為了獲取對(duì)象
//- Set()
//- Get()
//- Del()
//- Save()
//對(duì)象
type RedisSession struct {
sessionId string
pool *redis.Pool
//設(shè)置session,可以先放在內(nèi)存的map中
//批量的導(dǎo)入redis烫饼,提升性能
sessionMap map[string]interface{}
//讀寫鎖
rwlock sync.RWMutex
//記錄內(nèi)存中map是否被操作
flag int
}
//用常量定義狀態(tài)
const(
//內(nèi)存數(shù)據(jù)沒變化
SessionFlagNone = iota
//有變化
SessionFlagModify
)
//構(gòu)造函數(shù)
func NewRedisSession(id string,pool *redis.Pool) *RedisSession{
s := &RedisSession{
sessionId: id,
sessionMap: make(map[string]interface{},16),
pool: pool,
flag: SessionFlagNone,
}
return s
}
func (r *RedisSession)Set(key string,value interface{})(err error){
//加鎖
r.rwlock.Lock()
defer r.rwlock.Unlock()
//設(shè)置值
r.sessionMap[key] = value
//標(biāo)記
r.flag = SessionFlagModify
return
}
func (r *RedisSession)Get(key string)(value interface{},err error){
//加鎖
r.rwlock.Lock()
defer r.rwlock.Unlock()
//先判斷內(nèi)存
value,ok := r.sessionMap[key]
if !ok {
err = errors.New("key not exists in session")
return
}
return
}
//從redis再次加載
func (r *RedisSession)loadFromRedis()(err error){
conn := r.pool.Get()
reply,err := conn.Do("GET",r.sessionId)
if err != nil{
return
}
data,err := redis.String(reply,err)
if err != nil{
return
}
//取到的東西反序列化到內(nèi)存的map
err = json.Unmarshal([]byte(data),&r.sessionMap)
if err != nil{
return
}
return
}
func (r *RedisSession)Del(key string)(err error){
//加鎖
r.rwlock.Lock()
defer r.rwlock.Unlock()
r.flag = SessionFlagModify
delete(r.sessionMap,key)
return
}
//session存到redis
func (r *RedisSession)Save()(err error){
//加鎖
r.rwlock.Lock()
defer r.rwlock.Unlock()
//若數(shù)據(jù)沒變猎塞,不需要存
if r.flag != SessionFlagModify{
return
}
//內(nèi)存中的sessionMap進(jìn)行序列化
data,err := json.Marshal(r.sessionMap)
if err != nil{
return
}
//獲取redis連接
conn := r.pool.Get()
//保存kv
_,err = conn.Do("SET",r.sessionId,string(data))
//改狀態(tài)
r.flag = SessionFlagNone
if err != nil{
return
}
return
}
sessionMgr.go
package session
import (
"errors"
"github.com/gomodule/redigo/redis"
uuid "github.com/satori/go.uuid"
"sync"
"time"
)
//- 定義RedisSessionMgr對(duì)象(字段:存放所有session的map,讀寫鎖)
//- 構(gòu)造函數(shù)
//- Init():初始化杠纵,加載redis地址
//- CreateSession():創(chuàng)建一個(gè)新的session
//- GetSession(): 通過sessionID獲取對(duì)應(yīng)的session對(duì)象value
type RedisSessionMgr struct {
//redis地址
addr string
//密碼
password string
//連接池
pool *redis.Pool
//鎖
rwlock sync.RWMutex
//大map
sessionMap map[string]Session
}
//構(gòu)造
func NewRedisSessionMgr()SessionMgr{
sr := &RedisSessionMgr{
sessionMap: make(map[string]Session,32),
}
return sr
}
func (r *RedisSessionMgr)Init(addr string,options ...string)(err error){
//若有其他參數(shù)
if len(options) > 0{
r.password = options[0]
}
//創(chuàng)建連接池
r.pool = myPool(addr,r.password)
r.addr = addr
return
}
func myPool(addr,password string)*redis.Pool{
return &redis.Pool{
MaxIdle: 64,
MaxActive: 1000,
IdleTimeout: 240*time.Second,
Dial: func()(redis.Conn,error){
conn,err := redis.Dial("tcp",addr)
if err != nil{
return nil,err
}
//若有密碼荠耽,判斷
if _,err := conn.Do("AUTH",password);err != nil{
conn.Close()
return nil,err
}
return conn,err
},
//連接測(cè)試,開發(fā)時(shí)寫
//上線注釋掉
TestOnBorrow:func(conn redis.Conn,t time.Time)error{
_,err := conn.Do("PING")
return err
},
}
}
func (r *RedisSessionMgr)CreateSession()(session Session,err error){
r.rwlock.Lock()
defer r.rwlock.Unlock()
//用uuid作為sessionid
id := uuid.NewV4()
//將uuid轉(zhuǎn)成string
sessionId := id.String()
//創(chuàng)建個(gè)session
session = NewRedisSession(sessionId,r.pool)
//加到大map
r.sessionMap[sessionId] = session
return
}
func (r *RedisSessionMgr)Get(sessionId string)(session Session,err error){
r.rwlock.Lock()
defer r.rwlock.Unlock()
session,ok := r.sessionMap[sessionId]
if !ok{
err = errors.New("session not exists")
}
return
}
init.go
package session
import "fmt"
var(
sessionMgr SessionMgr
)
//中間件讓用戶去選擇使用哪個(gè)版本
func Init(provider string,addr string,options ...string)(err error){
switch provider {
case "memory":
sessionMgr = NewMemorySessionMgr()
case "redis":
sessionMgr = NewRedisSessionMgr()
default:
fmt.Errorf("不支持")
return
}
err = sessionMgr.Init(addr,options...)
return
}
數(shù)據(jù)庫(kù)
-
練習(xí)
image.png
CREATE TABLE `book`(
`id` INT(50) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(50) DEFAULT NULL,
`price` INT(50) DEFAULT NULL,
PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `book`(`id`,`title`,`price`) VALUES(1,'java',50),(2,'go',100),(3,'c',150);
book_list.html
{{define "book_list2.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>書籍列表</title>
</head>
<body>
<div>
<a href="/book/new">添加新書</a>
</div>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>title</th>
<th>price</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{{range .data}}
<tr>
<td>{{.ID}}</td> {{.ID}}
<td>{{.Title}}</td> {{.ID}}
<td>{{.Price}}</td> {{.ID}}
<td><a href="/book/delete?id={{.ID}}}">刪除</a></td>
</tr>
{{end}}
</tbody>
</table>
</body>
</html>
{{end}}
new_book.html
{{define "new_book.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加圖書信息</title>
</head>
<body>
<form action="/book/new" method="POST">
<div>
<label>書名:
<input type="text" name="title">
</label>
</div>
<div>
<label>價(jià)格:
<input type="number" name="price">
</label>
</div>
<div>
<label>書名:
<input type="submit" value="點(diǎn)我">
</label>
</div>
</form>
</body>
</html>
{{end}}
db.go
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var db *sqlx.DB
func initDB()(err error){
addr := "root:root@tcp(127.0.0.1:3306)/go"
db,err = sqlx.Connect("mysql",addr)
if err != nil{
return err
}
//最大連接
db.SetMaxOpenConns(100)
//最大空閑
db.SetMaxIdleConns(16)
return
}
func queryAllBook()(bookList []*Book,err error){
sqlStr := "select id,title,price from book"
err = db.Select(&bookList,sqlStr)
if err != nil{
fmt.Println("查詢失敗")
return
}
return
}
func insertBook(title string,price int64)(err error){
sqlStr := "insert into book(title,price) values(?,?)"
_,err = db.Exec(sqlStr,title,price)
if err != nil{
fmt.Println("插入失敗")
return
}
return
}
func deleteBook(id int64)(err error){
sqlStr := "delete from book where id = ? "
_,err = db.Exec(sqlStr,id)
if err != nil{
fmt.Println("刪除失敗")
return
}
return
}
model.go
package main
//定義書
type Book struct {
ID int64 `db:"id"`
Title string `db:"title"`
Price int64 `db:"price"`
}
main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main(){
//初始化數(shù)據(jù)庫(kù)
err := initDB()
if err != nil{
panic(err)
}
//定義路由
r := gin.Default()
//加載頁面
r.LoadHTMLGlob("./templates/*")
//查詢圖書
r.GET("/book/list",bookListHandler)
r.Run(":8000")
}
func bookListHandler(c *gin.Context) {
booklist,err := queryAllBook()
if err != nil{
c.JSON(http.StatusOK,gin.H{
"code":1,
"msg":err,
})
return
}
//返回?cái)?shù)據(jù)
c.HTML(http.StatusOK,"book_list2.html",gin.H{
"code":0,
"data":booklist,
})
}