gin框架

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)化”。

推薦閱讀阮一峰 理解RESTful架構(gòu)

簡(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.tmplindex.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ù)完成綁定:

  1. 如果是 GET 請(qǐng)求奖蔓,只使用 Form 綁定引擎(query)赞草。
  2. 如果是 POST 請(qǐng)求,首先檢查 content-type 是否為 JSONXML吆鹤,然后再使用 Formform-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)使用了LoggerRecovery中間件祈惶,其中:

  • 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路由

  1. 基本路由
  1. 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")
}

  1. 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){

}
  1. 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")
}

  1. 表單參數(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):
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>
        密&nbsp&nbsp碼:<input type="password" name="password">
        興&nbsp&nbsp趣:
        <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>
  1. 上傳單個(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>
  1. 上傳多個(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")
}

  1. routes group
    1. 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))
}
image.png
  1. 路由原理
    1. 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")
}

image.png

表單數(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>
        密&nbsp&nbsp碼:<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")
}

image.png

各種數(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)
    })

中間件

image.png
  • 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")
}

image.png
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里傳給客戶端就可以舍败。
image.png
  • 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è)版本)
image.png
  • 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
image.png

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
image.png

image.png

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);

image.png

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,
    })
}


image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市比藻,隨后出現(xiàn)的幾起案子铝量,更是在濱河造成了極大的恐慌,老刑警劉巖银亲,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慢叨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡务蝠,警方通過查閱死者的電腦和手機(jī)拍谐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馏段,“玉大人轩拨,你說我怎么就攤上這事≡合玻” “怎么了亡蓉?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)喷舀。 經(jīng)常有香客問我砍濒,道長(zhǎng),這世上最難降的妖魔是什么元咙? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任梯影,我火速辦了婚禮,結(jié)果婚禮上庶香,老公的妹妹穿的比我還像新娘甲棍。我一直安慰自己,他們只是感情好赶掖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布感猛。 她就那樣靜靜地躺著,像睡著了一般奢赂。 火紅的嫁衣襯著肌膚如雪陪白。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天膳灶,我揣著相機(jī)與錄音咱士,去河邊找鬼立由。 笑死,一個(gè)胖子當(dāng)著我的面吹牛序厉,可吹牛的內(nèi)容都是我干的锐膜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼弛房,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼道盏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起文捶,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤荷逞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后粹排,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體种远,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年恨搓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了院促。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斧抱,死狀恐怖常拓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辉浦,我是刑警寧澤弄抬,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站宪郊,受9級(jí)特大地震影響掂恕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弛槐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一懊亡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乎串,春花似錦店枣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至长豁,卻和暖如春钧唐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背匠襟。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工钝侠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留该园,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓机错,卻偏偏與公主長(zhǎng)得像爬范,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子弱匪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354