Golang GinWeb框架8-重定向/自定義中間件/認證/HTTPS支持/優雅重啟等
簡介
本文接著上文(Golang GinWeb框架7-靜態文件/模板渲染)繼續探索GinWeb框架.
重定向
Gin返回一個HTTP重定向非常簡單, 使用Redirect方法即可. 內部和外部鏈接都支持.
- package main
- import (
- "github.com/gin-gonic/gin"
- "net/http"
- )
- func main() {
- r := gin.Default()
- r.GET("/test", func(c *gin.Context) {
- c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") //重定向到外部鏈接
- })
- //重定向到內部鏈接
- r.GET("/internal", func(c *gin.Context) {
- c.Redirect(http.StatusMovedPermanently, "/home")
- })
- r.GET("/home", func(c *gin.Context) {
- c.JSON(200, gin.H{"msg": "這是首頁"})
- })
- r.Run(":8080")
- }
- /*
- 重定向到外部鏈接,訪問:http://localhost:8080/test
- 重定向到內部鏈接,訪問:http://localhost:8080/internal
- */
從POST方法中完成HTTP重定向, 參考問題#444 https://github.com/gin-gonic/gin/issues/444
- r.POST("/test", func(c *gin.Context) {
- c.Redirect(http.StatusFound, "/foo")
- })
如果要產生一個路由重定向, 類似上面的內部重定向, 則使用 HandleContext方法, 像下面這樣使用:
- r.GET("/test", func(c *gin.Context) {
- c.Request.URL.Path = "/test2"
- r.HandleContext(c)
- })
- r.GET("/test2", func(c *gin.Context) {
- c.JSON(200, gin.H{"hello": "world"})
- })
自定義中間件
- package main
- import (
- "github.com/gin-gonic/gin"
- "log"
- "time"
- )
- //自定義日志中間件
- func Logger() gin.HandlerFunc {
- return func(c *gin.Context) {
- t := time.Now()
- // Set example variable 在gin上下文中設置鍵值對
- c.Set("example", "12345")
- // before request
- //Next方法只能用于中間件中,在當前中間件中, 從方法鏈執行掛起的處理器
- c.Next()
- // after request 打印中間件執行耗時
- latency := time.Since(t)
- log.Print(latency)
- // access the status we are sending 打印本中間件的狀態碼
- status := c.Writer.Status()
- log.Println(status)
- }
- }
- func main() {
- r := gin.New()
- //使用該自定義中間件
- r.Use(Logger())
- r.GET("/test", func(c *gin.Context) {
- example := c.MustGet("example").(string) //從上下文中獲取鍵值對
- // it would print: "12345"
- log.Println(example)
- })
- // Listen and serve on 0.0.0.0:8080
- r.Run(":8080")
- }
使用基本認證BasicAuth()中間件
- package main
- import (
- "github.com/gin-gonic/gin"
- "net/http"
- )
- // simulate some private data
- var secrets = gin.H{
- "foo": gin.H{"email": "foo@bar.com", "phone": "123433"},
- "austin": gin.H{"email": "austin@example.com", "phone": "666"},
- "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"},
- }
- func main() {
- r := gin.Default()
- // Group using gin.BasicAuth() middleware
- // gin.Accounts is a shortcut for map[string]string
- // 路由組authorized使用基本認證中間件, 參數為gin.Accounts,是一個map,鍵名是用戶名, 鍵值是密碼, 該中間件會將認證信息保存到cookie中
- authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
- "foo": "bar",
- "austin": "1234",
- "lena": "hello2",
- "manu": "4321",
- }))
- // /admin/secrets endpoint
- // hit "localhost:8080/admin/secrets
- authorized.GET("/secrets", func(c *gin.Context) {
- // get user, it was set by the BasicAuth middleware
- // 從cookie中獲取用戶認證信息, 鍵名為user
- user := c.MustGet(gin.AuthUserKey).(string)
- if secret, ok := secrets[user]; ok {
- c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
- } else {
- c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
- }
- })
- // Listen and serve on 0.0.0.0:8080
- r.Run(":8080")
- }
- /*
- 測試訪問:http://localhost:8080/admin/secrets
- */
在中間件中使用協程Goroutines
在中間件或者控制器中啟動新協程時, 不能直接使用原來的Gin上下文, 必須使用一個只讀的上下文副本
- package main
- import (
- "github.com/gin-gonic/gin"
- "log"
- "time"
- )
- func main() {
- r := gin.Default()
- r.GET("/long_async", func(c *gin.Context) {
- // create copy to be used inside the goroutine
- // 創建一個Gin上下文的副本, 準備在協程Goroutine中使用
- cCp := c.Copy()
- go func() {
- // simulate a long task with time.Sleep(). 5 seconds
- // 模擬長時間任務,這里是5秒
- time.Sleep(5 * time.Second)
- // note that you are using the copied context "cCp", IMPORTANT
- // 在中間件或者控制器中啟動協程時, 不能直接使用原來的上下文, 必須使用一個只讀的上線文副本
- log.Println("Done! in path " + cCp.Request.URL.Path)
- }()
- })
- r.GET("/long_sync", func(c *gin.Context) {
- // simulate a long task with time.Sleep(). 5 seconds
- time.Sleep(5 * time.Second)
- // since we are NOT using a goroutine, we do not have to copy the context
- // 沒有使用協程時, 可以直接使用Gin上下文
- log.Println("Done! in path " + c.Request.URL.Path)
- })
- // Listen and serve on 0.0.0.0:8080
- r.Run(":8080")
- }
- /*
- 模擬同步阻塞訪問:http://localhost:8080/long_sync
- 模擬異步非阻塞訪問:http://localhost:8080/long_async
- */
自定義HTTP配置
直接使用 http.ListenAndServe()方法:
- func main() {
- router := gin.Default()
- http.ListenAndServe(":8080", router)
- }
或者自定義HTTP配置
- func main() {
- router := gin.Default()
- s := &http.Server{
- Addr: ":8080",
- Handler: router,
- ReadTimeout: 10 * time.Second,
- WriteTimeout: 10 * time.Second,
- MaxHeaderBytes: 1 << 20,
- }
- s.ListenAndServe()
- }
支持Let'sEncrypt證書加密處理HTTPS
下面是一行式的LetsEncrypt HTTPS服務
- package main
- import (
- "log"
- "github.com/gin-gonic/autotls"
- "github.com/gin-gonic/gin"
- )
- func main() {
- r := gin.Default()
- // Ping handler
- r.GET("/ping", func(c *gin.Context) {
- c.String(200, "pong")
- })
- //一行式LetsEncrypt證書, 處理https
- log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
- }
自定義自動證書管理器autocert manager實例代碼:
- package main
- import (
- "log"
- "github.com/gin-gonic/autotls"
- "github.com/gin-gonic/gin"
- "golang.org/x/crypto/acme/autocert"
- )
- func main() {
- r := gin.Default()
- // Ping handler
- r.GET("/ping", func(c *gin.Context) {
- c.String(200, "pong")
- })
- m := autocert.Manager{
- Prompt: autocert.AcceptTOS, //Prompt指定一個回調函數有條件的接受證書機構CA的TOS服務, 使用AcceptTOS總是接受服務條款
- HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"), //HostPolicy用于控制指定哪些域名, 管理器將檢索新證書
- Cache: autocert.DirCache("/var/www/.cache"), //緩存證書和其他狀態
- }
- log.Fatal(autotls.RunWithManager(r, &m))
- }
詳情參考autotls包
使用Gin運行多個服務
可以在主函數中使用協程Goroutine運行多個服務, 每個服務端口不同, 路由分組也不同. 請參考這個問題, 嘗試運行以下示例代碼:
- 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,
- }
- g.Go(func() error {
- err := server01.ListenAndServe()
- if err != nil && err != http.ErrServerClosed {
- log.Fatal(err)
- }
- return err
- })
- g.Go(func() error {
- err := server02.ListenAndServe()
- if err != nil && err != http.ErrServerClosed {
- log.Fatal(err)
- }
- return err
- })
- if err := g.Wait(); err != nil {
- log.Fatal(err)
- }
- }
- /*
- 模擬訪問服務1:
- curl http://localhost:8080/
- {"code":200,"error":"Welcome server 01"}
- 模擬訪問服務2:
- curl http://localhost:8081/
- {"code":200,"error":"Welcome server 02"}
- */
優雅的關閉和重啟服務
有一些方法可以優雅的關閉或者重啟服務, 比如不應該中斷活動的連接, 需要優雅等待服務完成后才執行關閉或重啟. 你可以使用第三方包來實現, 也可以使用內置的包自己實現優雅關閉或重啟.
使用第三方包
fvbock/endless 包, 可以實現Golang HTTP/HTTPS服務的零停機和優雅重啟(Golang版本至少1.3以上)
我們可以使用fvbock/endless 替代默認的 ListenAndServe方法, 更多詳情, 請參考問題#296.
- router := gin.Default()
- router.GET("/", handler)
- // [...]
- endless.ListenAndServe(":4242", router)
其他替代包:
- manners: 一個優雅的Go HTTP服務, 可以優雅的關閉服務.
- graceful: 優雅的Go包, 能夠優雅的關閉一個http.Handler服務
- grace: 該包為Go服務實現優雅重啟, 零停機
手動實現
如果你使用Go1.8或者更高的版本, 你可能不需要使用這些庫. 可以考慮使用http.Server的內置方法Shutdown()來優雅關閉服務. 下面的示例描述了基本用法, 更多示例請參考這里
- // +build go1.8
- package main
- import (
- "context"
- "log"
- "net/http"
- "os"
- "os/signal"
- "syscall"
- "time"
- "github.com/gin-gonic/gin"
- )
- func main() {
- router := gin.Default()
- router.GET("/", func(c *gin.Context) {
- time.Sleep(5 * time.Second)
- c.String(http.StatusOK, "Welcome Gin Server")
- })
- srv := &http.Server{
- Addr: ":8080",
- Handler: router,
- }
- // Initializing the server in a goroutine so that
- // it won't block the graceful shutdown handling below
- // 用協程初始化一個服務, 它不會阻塞下面的優雅邏輯處理
- go func() {
- if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- log.Fatalf("listen: %s\n", err)
- }
- }()
- // Wait for interrupt signal to gracefully shutdown the server with
- // a timeout of 5 seconds.
- //等待一個操作系統的中斷信號, 來優雅的關閉服務
- quit := make(chan os.Signal)
- // kill (no param) default send syscall.SIGTERM //kill會發送終止信號
- // kill -2 is syscall.SIGINT //發送強制進程結束信號
- // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it //發送SIGKILL信號給進程
- signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
- <-quit //阻塞在這里,直到獲取到一個上面的信號
- log.Println("Shutting down server...")
- // The context is used to inform the server it has 5 seconds to finish
- // the request it is currently handling
- //這里使用context上下文包, 有5秒鐘的處理超時時間
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- if err := srv.Shutdown(ctx); err != nil { //利用內置Shutdown方法優雅關閉服務
- log.Fatal("Server forced to shutdown:", err)
- }
- log.Println("Server exiting")
- }
參考文檔
Gin官方倉庫:https://github.com/gin-gonic/gin