Golang 微框架 Gin 簡介


Gin

  Gin是一個golang的微框架,封裝比較優雅,API友好,源碼注釋比較明確,已經發布了1.0版本。具有快速靈活,容錯方便等特點。其實對於golang而言,web框架的依賴要遠比Python,Java之類的要小。自身的net/http足夠簡單,性能也非常不錯。框架更像是一些常用函數或者工具的集合。借助框架開發,不僅可以省去很多常用的封裝帶來的時間,也有助於團隊的編碼風格和形成規范。

下面就Gin的用法做一個簡單的介紹。

首先需要安裝,安裝比較簡單,使用go get即可:

go get -u github.com/gin-gonic/gin

Hello World

使用Gin實現Hello world非常簡單,創建一個router,然后使用其Run的方法:

import (
    "gopkg.in/gin-gonic/gin.v1"
    "net/http"
)

func main(){
    
    router := gin.Default()

    router.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello World")
    })
    router.Run(":8000")
}
簡單幾行代碼,就能實現一個web服務。使用gin的Default方法創建一個路由handler。然后通過HTTP方法綁定路由規則和路由函數。不同於net/http庫的路由函數,gin進行了封裝,把request和response都封裝到gin.Context的上下文環境。最后是啟動路由的Run方法監聽端口。麻雀雖小,五臟俱全。當然,除了GET方法,gin也支持POST,PUT,DELETE,OPTION等常用的restful方法。

restful路由

gin的路由來自httprouter庫。因此httprouter具有的功能,gin也具有,不過gin不支持路由正則表達式:

func main(){
    router := gin.Default()
    
    router.GET("/user/:name", func(c *gin.Context) {
        name := c.Param("name")
        c.String(http.StatusOK, "Hello %s", name)
    })
}

冒號:加上一個參數名組成路由參數。可以使用c.Params的方法讀取其值。當然這個值是字串string。諸如/user/rsj217,和/user/hello都可以匹配,而/user//user/rsj217/不會被匹配。

☁  ~  curl http://127.0.0.1:8000/user/rsj217
Hello rsj217%~  curl http://127.0.0.1:8000/user/rsj217/
404 page not found%~  curl http://127.0.0.1:8000/user/
404 page not found%

除了:,gin還提供了*號處理參數,*號能匹配的規則就更多。

func main(){
    router := gin.Default()
    
    router.GET("/user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        message := name + " is " + action
        c.String(http.StatusOK, message)
    })
}

訪問效果如下

☁  ~  curl http://127.0.0.1:8000/user/rsj217/
rsj217 is /%~  curl http://127.0.0.1:8000/user/rsj217/中國
rsj217 is /中國%

query string參數與body參數

web提供的服務通常是client和server的交互。其中客戶端向服務器發送請求,除了路由參數,其他的參數無非兩種,查詢字符串query string和報文體body參數。所謂query string,即路由用,用?以后連接的key1=value2&key2=value2的形式的參數。當然這個key-value是經過urlencode編碼。

query string

對於參數的處理,經常會出現參數不存在的情況,對於是否提供默認值,gin也考慮了,並且給出了一個優雅的方案:

func main(){
    router := gin.Default()
    router.GET("/welcome", func(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest")
        lastname := c.Query("lastname")

        c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    })
  router.Run()
}

使用c.DefaultQuery方法讀取參數,其中當參數不存在的時候,提供一個默認值。使用Query方法讀取正常參數,當參數不存在的時候,返回空字串:

☁  ~  curl http://127.0.0.1:8000/welcome
Hello Guest %~  curl http://127.0.0.1:8000/welcome\?firstname\=中國
Hello 中國 %~  curl http://127.0.0.1:8000/welcome\?firstname\=中國\&lastname\=天朝
Hello 中國 天朝%~  curl http://127.0.0.1:8000/welcome\?firstname\=\&lastname\=天朝
Hello  天朝%~  curl http://127.0.0.1:8000/welcome\?firstname\=%E4%B8%AD%E5%9B%BD
Hello 中國 %

之所以使用中文,是為了說明urlencode。注意,當firstname為空字串的時候,並不會使用默認的Guest值,空值也是值,DefaultQuery只作用於key不存在的時候,提供默認值。

body

http的報文體傳輸數據就比query string稍微復雜一點,常見的格式就有四種。例如application/jsonapplication/x-www-form-urlencoded, application/xmlmultipart/form-data。后面一個主要用於圖片上傳。json格式的很好理解,urlencode其實也不難,無非就是把query string的內容,放到了body體里,同樣也需要urlencode。默認情況下,c.PostFROM解析的是x-www-form-urlencodedfrom-data的參數。

func main(){
    router := gin.Default()
    router.POST("/form_post", func(c *gin.Context) {
        message := c.PostForm("message")
        nick := c.DefaultPostForm("nick", "anonymous")

        c.JSON(http.StatusOK, gin.H{
            "status":  gin.H{
                "status_code": http.StatusOK,
                "status":      "ok",
            },
            "message": message,
            "nick":    nick,
        })
    })
}

與get處理query參數一樣,post方法也提供了處理默認參數的情況。同理,如果參數不存在,將會得到空字串。

☁  ~  curl -X POST http://127.0.0.1:8000/form_post -H "Content-Type:application/x-www-form-urlencoded" -d "message=hello&nick=rsj217" | python -m json.tool

{
    "message": "hello",
    "nick": "rsj217",
    "status": {
        "status": "ok",
        "status_code": 200
    }
}

發送數據給服務端,並不是post方法才行,put方法一樣也可以。同時querystring和body也不是分開的,兩個同時發送也可以:

func main(){
    router := gin.Default()
    
    router.PUT("/post", func(c *gin.Context) {
        id := c.Query("id")
        page := c.DefaultQuery("page", "0")
        name := c.PostForm("name")
        message := c.PostForm("message")
        fmt.Printf("id: %s; page: %s; name: %s; message: %s \n", id, page, name, message)
        c.JSON(http.StatusOK, gin.H{
            "status_code": http.StatusOK,
        })
    })
}

上面的例子,展示了同時使用查詢字串和body參數發送數據給服務器。

表單

我們先要寫一個表單頁面,因此需要引入gin如何render模板。前面我們見識了c.String和c.JSON。下面就來看看c.HTML方法。

首先需要定義一個模板的文件夾。然后調用c.HTML渲染模板,可以通過gin.H給模板傳值。至此,無論是String,JSON還是HTML,以及后面的XML和YAML,都可以看到Gin封裝的接口簡明易用。

創建一個文件夾templates,然后再里面創建html文件login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
</head>
<body>
<h3>Login</h3>
<form action="/form_post", method="post" >
    <input type="text" name="name" />
    <input type="text" name="password" />
    <input type="submit" value="提交" />
</form>

</body>
</html>

使用LoadHTMLGlob定義模板文件路徑。

router.LoadHTMLGlob("templates/*")
    router.GET("/login", func(c *gin.Context) {
        c.HTML(http.StatusOK, "login.html", gin.H{})
    })

重定向

gin對於重定向的請求,相當簡單。調用上下文的Redirect方法:

  router.GET("/redict/google", func(c *gin.Context) {
        c.Redirect(http.StatusMovedPermanently, "https://google.com")
    })

分組路由

v1 := router.Group("/v1")

    v1.GET("/login", func(c *gin.Context) {
        c.String(http.StatusOK, "v1 login")
    })

    v2 := router.Group("/v2")

    v2.GET("/login", func(c *gin.Context) {
        c.String(http.StatusOK, "v2 login")
    })

訪問效果如下:

☁  ~  curl http://127.0.0.1:8000/v1/login
v1 login%~  curl http://127.0.0.1:8000/v2/login
v2 login%

 

middleware中間件

golang的net/http設計的一大特點就是特別容易構建中間件。gin也提供了類似的中間件。需要注意的是中間件只對注冊過的路由函數起作用。對於分組路由,嵌套使用中間件,可以限定中間件的作用范圍。中間件分為全局中間件,單個路由中間件和群組中間件。

全局中間件

先定義一個中間件函數:

//全局中間件
func MiddleWare() gin.HandlerFunc  {
    return func(c *gin.Context) {
        c.Set("salt", "qingdao")
        c.Next()
    }
}

該函數很簡單,只會給c上下文添加一個屬性,並賦值。后面的路由處理器,可以根據被中間件裝飾后提取其值。需要注意,雖然名為全局中間件,只要注冊中間件的過程之前設置的路由,將不會受注冊的中間件所影響。只有注冊了中間件一下代碼的路由函數規則,才會被中間件裝飾。

//使用中間件
router.Use(MiddleWare())
{
    router.GET("/middleware", func(c *gin.Context) {
        salt, _ := c.Get("salt")
        c.JSON(http.StatusOK, gin.H{
            "salt":salt,
        })
    })
}

使用router裝飾中間件,然后在/middlerware即可讀取request的值,注意在router.Use(MiddleWare())代碼以上的路由函數,將不會有被中間件裝飾的效果。

使用花括號包含被裝飾的路由函數只是一個代碼規范,即使沒有被包含在內的路由函數,只要使用router進行路由,都等於被裝飾了。想要區分權限范圍,可以使用組返回的對象注冊中間件。

☁  ~  curl  http://127.0.0.1:8000/middleware
{"salt":"qingdao"}

上面的注冊裝飾方式,會讓所有下面所寫的代碼都默認使用了router的注冊過的中間件。

單個路由中間件

當然,gin也提供了針對指定的路由函數進行注冊。

router.GET("/before", MiddleWare(), func(c *gin.Context) {
        request := c.MustGet("request").(string)
        c.JSON(http.StatusOK, gin.H{
            "middile_request": request,
        })
    })

群組中間件

群組的中間件也類似,只要在對於的群組路由上注冊中間件函數即可:

authorized := router.Group("/", MyMiddelware())
// 或者這樣用:
authorized := router.Group("/")
authorized.Use(MyMiddelware())
{
    authorized.POST("/login", loginEndpoint)
}

群組可以嵌套,因為中間件也可以根據群組的嵌套規則嵌套。

中間件實踐

中間件最大的作用,莫過於用於一些記錄log,錯誤handler,還有就是對部分接口的鑒權。下面就實現一個簡易的鑒權中間件。

router.GET("/auth/signin", func(c *gin.Context) {
        cookie := &http.Cookie{
            Name:     "session_id",
            Value:    "123",
            Path:     "/",
            HttpOnly: true,
        }
        http.SetCookie(c.Writer, cookie)
        c.String(http.StatusOK, "Login successful")
    })

    router.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"data": "home"})
    })

登錄函數會設置一個session_id的cookie,注意這里需要指定path為/,不然gin會自動設置cookie的path為/auth,一個特別奇怪的問題。/homne的邏輯很簡單,使用中間件AuthMiddleWare注冊之后,將會先執行AuthMiddleWare的邏輯,然后才到/home的邏輯。

AuthMiddleWare的代碼如下:

func AuthMiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        if cookie, err := c.Request.Cookie("session_id"); err == nil {
            value := cookie.Value
            fmt.Println(value)
            if value == "123" {
                c.Next()
                return
            }
        }
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Unauthorized",
        })
        c.Abort()
        return
    }
}

從上下文的請求中讀取cookie,然后校對cookie,如果有問題,則終止請求,直接返回,這里使用了c.Abort()方法。

In [7]: resp = requests.get('http://127.0.0.1:8000/home')

In [8]: resp.json()
Out[8]: {u'error': u'Unauthorized'}

In [9]: login = requests.get('http://127.0.0.1:8000/auth/signin')

In [10]: login.cookies
Out[10]: <RequestsCookieJar[Cookie(version=0, name='session_id', value='123', port=None, port_specified=False, domain='127.0.0.1', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]>

In [11]: resp = requests.get('http://127.0.0.1:8000/home', cookies=login.cookies)

In [12]: resp.json()
Out[12]: {u'data': u'home'}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM