Go Web開發(Gin框架)簡易入門教程

togettoyou發表於2020-11-20

在講述框架之前,先來說說Go語言的內建net/http包,其實net/http已經為我們提供了基礎的路由函式組合和豐富的功能函式,如果你只是需要簡單的API編寫,net/http就完全足夠了。
下面就來編寫一個最簡單的web服務程式:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    // 該方法接收一個路由匹配的字串,以及一個 func(ResponseWriter, *Request) 型別的函式
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8000", nil)) // 監聽本地8000埠
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) // r.URL.Path 輸出url的路徑
}

瀏覽器訪問結果:
image.png
來進階一下,看看如何解析常見的請求引數型別,以及如何返回json格式。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

func main() {
    // 該方法接收一個路由匹配的字串,以及一個 func(ResponseWriter, *Request) 型別的函式
    http.HandleFunc("/", handler)
    http.HandleFunc("/get", handleGet)
    http.HandleFunc("/postJson", handlePostJson)
    http.HandleFunc("/postForm", handlePostForm)
    http.HandleFunc("/responseJson", handleResponseJson)
    log.Fatal(http.ListenAndServe(":8000", nil)) // 監聽本地8000埠
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) // r.URL.Path 輸出url的路徑
}

// 處理GET請求查詢引數
func handleGet(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query()
    id := query.Get("id")
    fmt.Fprintf(w, "GET: id=%s\n", id)
}

// 處理application/json型別的POST請求
func handlePostJson(w http.ResponseWriter, r *http.Request) {
    // 根據請求body建立一個json解析器例項
    decoder := json.NewDecoder(r.Body)
    // 用於存放引數key=value資料
    var params map[string]string
    // 解析引數 存入map
    decoder.Decode(&params)
    fmt.Fprintf(w, "POST json: username=%s, password=%s\n", params["username"], params["password"])
}

// 處理application/x-www-form-urlencoded型別的POST請求
func handlePostForm(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    username := r.Form.Get("username")
    password := r.Form.Get("password")
    fmt.Fprintf(w, "POST form-urlencoded: username=%s, password=%s\n", username, password)
}

// 返回JSON資料格式
func handleResponseJson(w http.ResponseWriter, r *http.Request) {
    type Response struct {
        Code int         `json:"code"`
        Msg  string      `json:"msg"`
        Data interface{} `json:"data"`
    }
    res := Response{
        200,
        "success",
        "admin",
    }
    json.NewEncoder(w).Encode(res) // 關鍵
}

/get請求結果:
image.png
/postJson請求結果:
image.png
/postForm請求結果:
image.png
/responseJson請求結果:
image.png
到這裡,基本的使用就已經介紹的差不多了,關於更多net/http標準庫的內容可以直接檢視標準庫文件:studygolang.com/pkgdoc

引入官方的介紹:
Gin 是一個用 Go (Golang) 編寫的 HTTP web 框架。 它是一個類似於 martini 但擁有更好效能的 API 框架,由於 httprouter,速度提高了近 40 倍。如果你需要極好的效能,使用 Gin 吧。

特性:

  • 快速:路由不使用反射,基於Radix樹,記憶體佔用少。
  • 中介軟體:HTTP請求,可先經過一系列中介軟體處理,例如:Logger,Authorization,GZIP等。這個特性和 NodeJs 的 Koa 框架很像。中介軟體機制也極大地提高了框架的可擴充套件性。
  • 異常處理:服務始終可用,不會當機。Gin 可以捕獲 panic,並恢復。而且有極為便利的機制處理HTTP請求過程中發生的錯誤。
  • JSON:Gin可以解析並驗證請求的JSON。這個特性對Restful API的開發尤其有用。
  • 路由分組:例如將需要授權和不需要授權的API分組,不同版本的API分組。而且分組可巢狀,且效能不受影響。
  • 渲染內建:原生支援JSON,XML和HTML的渲染。

1.快速開始

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // 預設監聽本地8080埠,如果需要更改可以使用 r.Run(":9000")
}

瀏覽器訪問:
image.png
控制檯輸出:image.png
在這段程式碼中gin做了什麼?

  1. 首先,我們使用了gin.Default()生成了一個例項,賦值給r
  2. 接下來,我們使用r.Get("/ping", ...)宣告瞭一個路由,告訴 Gin 什麼樣的URL 能觸發傳入的函式,這個函式返回我們想要顯示在使用者瀏覽器中的資訊。
  3. 最後用 r.Run()函式來讓應用執行在本地伺服器上,預設監聽埠是 8080,可以傳入引數設定埠,例如r.Run(":9999")即執行在9999埠。

2.路由

路由方法有 GET, POST, PUT, PATCH, DELETEOPTIONS,還有Any,可匹配以上任意型別的請求。

r.GET("/someGet", func)
r.POST("/somePost", func)
r.PUT("/somePut", func)
r.DELETE("/someDelete", func)
r.PATCH("/somePatch", func)
r.HEAD("/someHead", func)
r.OPTIONS("/someOptions", func)
//處理所有的請求方法
r.Any("/any", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "請求型別": c.Request.Method,
    })
})

路由引數

// 無引數
r.GET("/", func (c *gin.Context)  {
    c.String(http.StatusOK, "Hello")
})
// 路徑引數,匹配 /path/admin
r.GET("/path/:name", func(c *gin.Context) {
    name := c.Param("name")//取得URL路徑中引數name的值
    c.String(http.StatusOK, "Hello %s", name)
})
// 星號路由引數,匹配所有,不建議使用,如/all/*id
r.GET("/all/*id", func(c *gin.Context) {
    id := c.Param("id")
    c.String(http.StatusOK, "id is  %s", id)
})
// 查詢引數,匹配 user?name=xxx&role=xxx,role可選
r.GET("/user", func(c *gin.Context) {
    name := c.Query("name")//查詢請求URL後面的引數name的值
    role := c.DefaultQuery("role", "teacher")//如果獲取不到,會賦值預設值"teacher"
    c.String(http.StatusOK, "%s is a %s", name, role)
})
// form表單
r.POST("/form", func(c *gin.Context) {
    username := c.PostForm("username")
    password := c.DefaultPostForm("password", "000000") // 可設定預設值
    c.JSON(http.StatusOK, gin.H{
        "username": username,
        "password": password,
    })
})
// json引數
r.POST("/json", func(c *gin.Context) {
    type Body struct {
        Email    string `json:"email"`
        Username string `json:"username"`
    }
    var body Body
    c.ShouldBind(&body)//繫結引數,將引數解析到body結構體中
    c.JSON(http.StatusOK, body)
})
// 陣列引數,匹配多選業務如 array?answer=xxx&answer=xxx&answer=xxx,key一樣,value不同
r.GET("/array", func(c *gin.Context) {
    array := c.QueryArray("answer")
    c.JSON(http.StatusOK, array)
})
// map引數,字典引數,匹配 map?ids[a]=123&ids[b]=456&ids[c]=789
r.GET("/map", func(c *gin.Context) {
    c.JSON(http.StatusOK, c.QueryMap("ids"))
})

路由分組

v1Group := r.Group("/v1")
{
    v1Group.GET("/user", func(c *gin.Context) {
        c.String(200, "這是v1版本/v1/user")
    })
}

v2Group := r.Group("/v2")
{
    v2Group.GET("/user", func(c *gin.Context) {
        c.String(200, "這是v2版本/v2/user")
    })
}

3.輸出渲染格式

gin可以很方便的渲染輸出資料的格式

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

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

    r.GET("/someString", func(c *gin.Context) {
        c.String(http.StatusOK, "string")
    })

    // gin.H 是 map[string]interface{} 的一種快捷方式
    r.GET("/someJSON", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    })

    r.GET("/moreJSON", func(c *gin.Context) {
        // 你也可以使用一個結構體
        var msg struct {
            Name    string `json:"user"`
            Message string
            Number  int
        }
        msg.Name = "Lena"
        msg.Message = "hey"
        msg.Number = 123
        // 注意由於`json:"user"`的關係 msg.Name 在 JSON 中變成了 "user"
        // 將輸出:{"user": "Lena", "Message": "hey", "Number": 123}
        c.JSON(http.StatusOK, msg)
    })

    r.GET("/someXML", func(c *gin.Context) {
        c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    })

    r.GET("/someYAML", func(c *gin.Context) {
        c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    })

    r.Run()
}

4.中介軟體

回到快速開始中,我們使用gin.Default()生成了一個例項,檢視gin.Default()的原始碼,可以發現
image.png
Default函式會預設繫結兩個已經準備好的中介軟體,LoggerRecovery,分別幫助我們列印日誌輸出和painc處理。
從中可以看到,Gin的中介軟體是通過Use方法設定的,它接收一個可變引數,所以我們同時可以設定多箇中介軟體。
image.png
而一個Gin的中介軟體,其實就是Gin定義的一個HandlerFunc
image.png
它在我們Gin中經常使用,比如:

// 其中func(c *gin.Context){} 就是一個HandlerFunc型別函式
r.GET("/", func(c *gin.Context) {
    c.String(200, "hello")
})

我們現在來嘗試如何使用中介軟體:

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    // 建立一個不包含中介軟體的路由器例項
    r := gin.New()

    // 全域性使用中介軟體
    // 使用gin自帶的 Logger 日誌中介軟體
    r.Use(gin.Logger())
    // 使用gin自帶的 Recovery 中介軟體從任何 panic 恢復,如果出現 panic,它會寫一個 500 錯誤。
    r.Use(gin.Recovery())

    // 以上程式碼相當於 r := gin.Default()

    // 新增自定義的全域性中介軟體
    r.Use(middleware.Cors())

    // 單個路由新增中介軟體,可以新增任意多個
    r.GET("/path", middleware.JWT())

    // 路由組中新增中介軟體,中介軟體只在該路由組中產生作用
    // user := r.Group("/user", middleware.AuthRequired())
    user := r.Group("/user")
    user.Use(middleware.AuthRequired())
    {
        user.POST("/login", Login)
    }
    r.Run()
}

學會使用中介軟體後,如何自定義一個自己的中介軟體呢?很簡單,比如想要自定義一個計算請求時間的requestTime中介軟體:

package main

import (
    "fmt"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.Use(requestTime())
    r.GET("/", func(c *gin.Context) {
        fmt.Println("我到這裡了 hello")
        c.String(200, "hello")
    })
    r.Run()
}

func requestTime() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now() // 記錄開始時間
        fmt.Println("這裡被我攔截住了")
        fmt.Println("因為呼叫了c.Next() 我現在要走了")
        c.Next() // 立即呼叫下一個HandlerFunc(會產生呼叫耗時)
        fmt.Println("我又回來了")
        fmt.Println(time.Since(start)) // 列印本次請求處理時間差
    }
}

image.png
可以看到c.Next()在這裡發揮了很大的作用,其中在c.Next()之前的操作我們一般用來做驗證處理,訪問是否允許之類的。 之後的操作一般就是用來做總結處理,比如格式化輸出、響應結束時間,響應時長計算之類的。

在gin中介軟體中,除了c.Next(),相對應的還有c.Abort(),而c.Abort()的作用則是阻止後續的處理函式,比如檢驗到是非法請求時,阻斷接下來的操作。

package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.Use(authority())
    r.GET("/path/:name", func(c *gin.Context) {
        fmt.Println("歡迎")
        c.String(http.StatusOK, "Hello %s", c.Param("name"))
    })
    r.Run()
}

func authority() gin.HandlerFunc {
    return func(c *gin.Context) {
        fmt.Println("開始-許可權控制")
        isAdmin := c.Param("name") == "admin"
        if isAdmin {
            c.Next()
        } else {
            c.Abort() //不會再執行接下去的HandlerFunc了
            c.String(http.StatusOK, "對不起,您不是管理員")
        }
        fmt.Println("結束-許可權控制")
    }
}

情況①:
image.png
image.png
情況②:
image.png
image.png
程式在呼叫了c.Abort()之後,就只會繼續往下執行中介軟體的程式碼,不會再跳到我們定義的介面HandlerFunc去了,其實就是起到了攔截的作用。

總結一下c.Next()作用是立即執行下一個HandlerFunc,完後會跳轉回來繼續執行c.Next()接下去的程式碼,
c.Abort()則是阻斷執行下一個HandlerFunc,僅會執行接下去的程式碼,常用於許可權控制攔截操作。

有時候我們還需要跨中介軟體取值,可以使用c.Set(key, value)設定值,使用c.Get(key)取值

package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.Use(authority())
    r.GET("/", func(c *gin.Context) {
        value, ok := c.Get("key")
        if ok {
            fmt.Println(value)
        }
        c.String(http.StatusOK, "Hello")
    })
    r.Run()
}

func authority() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("key", "你好")
        c.Next()
    }
}

請求結果:
image.png

基本介紹到這裡,關於gin的文件可以參考:《Gin 框架中文文件》

我的開源專案

github.com/togettoyou/go-one-serve...
基於Gin進行快速構建RESTful API 服務的專案模板

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章