Gin學習筆記01 框架使用

IT小馬發表於2022-01-09

Go Web

net/http庫

package main

import (
    "fmt"
    "net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}
func main() {
    http.HandleFunc("/hello", sayHello)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        fmt.Println("http server failed, err:", err)
    }
}

開啟服務

go run main.go

訪問瀏覽器

http://localhost:9090/hello

http/template庫

模板檔案 hello.tmpl

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Hello</title>
</head>
<body>
    <p>{{ . }}</p>
</body>
</html>

main.go

func sayHello(w http.ResponseWriter, r *http.Request) {
    //解析模板
    t, _ := template.ParseFiles("./hello.tmpl")
    //渲染模板
    t.Execute(w, "World")
}
func main() {
    http.HandleFunc("/", sayHello)
    http.ListenAndServe(":9091", nil)
}
引數渲染
t.Execute(w, map[string]interface{}{
        "a": "aaa",//{{ .a }}
        "b": "bbb",//{{ .b }}
})
模板註釋
{{/* .a */}}
條件判斷
{{ if lt .a 1}}
a < 1
{{ else }}
a >= 1
{{ end }}  
迴圈
t.Execute(w, map[string]interface{}{
        "num": []string{"a", "b", "c"},
})

{{ range $k,$v := .num}}
<p>{{ $k }} - {{ $v }}</p>
{{ else }}
---
{{ end }}  
繼承
t, err := template.ParseFiles("./base.tmpl","./index.tmpl")
name := "AAA"
t.ExecuteTemplate(w, "index.tmpl", name)

base.tmpl

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Go Templates</title>
</head>
<body>
<div class="container-fluid">
    {{block "content" . }}{{end}}
</div>
</body>
</html>

index.tmpl

{{template "base.tmpl"}}

{{define "content"}}
    <div>Hello world!</div>
{{end}}
修改識別符號
template.New("index.tmpl").Delims("{[","]}").ParseFiles("./index.tmpl")
自定義函式
t, _ := template.New("xss.tmpl").Funcs(template.FuncMap{
    "safe": func(s string) template.HTML {
        return template.HTML(s)
    },
}).ParseFiles("./xss.tmpl")
str := "<a>123</a>"
t.Execute(w, str)

sss.tmpl

{{ . | safe }}

Gin框架

安裝與使用

配置代理

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

引入gin庫

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

編寫Demo

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(":8081") // 監聽並在 0.0.0.0:8081 上啟動服務
}

解決:no required module provides package github.com/gin-gonic/gin: go.mod file not found in current directory or any parent directory; see 'go help modules'

go mod init xxx
go get github.com/gin-gonic/gin

解決VSCODE出現紅色波浪線的問題:

go mod vendor

訪問瀏覽器

http://localhost:8081/ping

HTML渲染

Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法進行HTML模板渲染。

main.go

package main
import (
    "net/http"
    "github.com/gin-gonic/gin"
)
func main() {
    r := gin.Default()
    // r.LoadHTMLGlob("template/**/*")
    r.LoadHTMLFiles("template/user/index.html")
    r.GET("/user/index", func(c *gin.Context) {
        c.HTML(http.StatusOK, "user/index.html", gin.H{
            "title": "User Index",
        })
    })
    r.Run(":8081") // 監聽並在 0.0.0.0:8081 上啟動服務
}

template/user/index.html

{{define "user/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>{{.title}}</title>
</head>
<body>
    {{.title}}
</body>
</html>
{{end}}

自定義函式

r.SetFuncMap(template.FuncMap{
        "safe": func(str string) template.HTML {
            return template.HTML(str)
        },
})

對應模板

{{.link| safe}}

載入靜態檔案

在解析檔案(LoadHTMLGlob)前載入靜態檔案
r.Static("/xxx", "./statics") //以xxx開頭的訪問解析到./statics目錄

模板檔案

<link rel="stylesheet" href="xxx/index.css"/>

模板繼承

Gin框架預設都是使用單模板,如果需要使用block template功能,可以通過"github.com/gin-contrib/multitemplate"庫實現
go get -u github.com/gin-contrib/multitemplate

home.tmpl和index.tmpl繼承了base.tmpl

templates
├── includes
│   ├── home.tmpl
│   └── index.tmpl
├── layouts
│   └── base.tmpl

載入函式

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
}

func main() {
    r := gin.Default()
    r.HTMLRender = loadTemplates("./templates")
    r.LoadHTMLGlob("templates/**/*")
    r.GET("/ping", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.tmpl", nil)
    })
    r.Run(":8082") // 監聽並在 0.0.0.0:8081 上啟動服務
}

獲取當前程式路徑

func getCurrentPath() string {
    if ex, err := os.Executable(); err == nil {
        return filepath.Dir(ex)
    }
    return "./"
}

JSON渲染

gin.H 是map[string]interface{}的縮寫
func main() {
    r := gin.Default()
  // 方式一:自己拼接JSON {"message":"Hello World"}
    r.GET("/json", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World",
        })
    })
    //方法二:使用結構體 {"name":"test"}
    var msg struct {
        Name string `json:"name"`
    }
    msg.Name = "test"
    r.GET("/json2", func(c *gin.Context) {
        c.JSON(http.StatusOK, msg)
    })
    r.Run(":8082") // 監聽並在 0.0.0.0:8082 上啟動服務
}

XML,YMAL,protobuf渲染

c.XML(http.StatusOK, msg)
c.YMAL(http.StatusOK, msg)
c.ProtoBuf(http.StatusOK, data)

Gin引數

query引數解析

http://localhost:8082/web?que...

func main() {
    r := gin.Default()
    r.GET("/web", func(c *gin.Context) {
        // query := c.Query("query")
        // query := c.DefaultQuery("query", "default")//設定預設值
        query, ok := c.GetQuery("query") //ok表示是否獲取到引數
        if !ok {
            query = "default"
        }
        c.JSON(http.StatusOK, gin.H{
            "query": query,
        })
    })
    r.Run(":8082")
}

form引數解析

login.html

func main() {
    r := gin.Default()
    r.LoadHTMLFiles("login.html")
    r.GET("/login", func(c *gin.Context) {
        c.HTML(http.StatusOK, "login.html", nil)
    })
    r.POST("/login", func(c *gin.Context) {
        // username := c.PostForm("username")
        // password := c.PostForm("password") //取到值則返回,取不到返回空
        // username := c.DefaultPostForm("username", "aaa")
        // password := c.DefaultPostForm("password", "bbb") //預設值
        username, ok := c.GetPostForm("username")
        if !ok {
            username = "xxx"
        }
        password, ok := c.GetPostForm("password")
        if !ok {
            password = "yyy"
        }
        c.JSON(http.StatusOK, gin.H{
            "username": username,
            "password": password,
        })
    })
    r.Run(":8082")
}

Json引數解析

func main() {
    r := gin.Default()
    r.POST("/json", func(c *gin.Context) {
        body, _ := c.GetRawData()
        var m gin.H //map[string] interface{}
        _ = json.Unmarshal(body, &m) //Unmarshal時接收體必須傳遞指標
        c.JSON(http.StatusOK, m)
    })
    r.Run(":8082")
}

Path引數解析

訪問路徑:http://localhost:8082/itxiaom...

func main() {
    r := gin.Default()
    r.GET("/:name/:id", func(c *gin.Context) {
        name := c.Param("name")
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{
            "name": name,
            "id":   id,
        })
    })
    r.Run(":8082")
}

引數繫結

ShouldBind會按照下面的順序解析請求中的資料完成繫結:

  1. 如果是 GET 請求,只使用 Form 繫結引擎(query)。
  2. 如果是 POST 請求,首先檢查 content-type 是否為 JSONXML,然後再使用 Formform-data)。
type Login struct {
    Username string `form:"username" json:"user" binding:"required"`
    Password string `form:"password" json:"pwd" binding:"required"`
}

func main() {
    r := gin.Default()
  r.POST("/get", func(c *gin.Context) {
        var login Login
        err := c.ShouldBind(&login)//自動解析query
        if err == nil {
            c.JSON(http.StatusOK, login)
        }
    })
    r.POST("/login", func(c *gin.Context) {
        var login Login
        err := c.ShouldBind(&login)//自動解析json,form             
        if err == nil {
            c.JSON(http.StatusOK, login)
        }
    })
    r.Run(":8082")
}

檔案上傳

<form action="/upload" method="post" enctype="multipart/form-data">
    <input name="filename" type="file"/>
    <button type="submit">Submit</button>
</form>

單個檔案上傳

func main() {
    r := gin.Default()
    r.LoadHTMLFiles("upload.html")
    r.GET("/upload", func(c *gin.Context) {
        c.HTML(http.StatusOK, "upload.html", nil)
    })
    r.POST("/upload", func(c *gin.Context) {
        //從請求中讀取檔案
        file, _ := c.FormFile("filename")
        //將檔案儲存到服務端
        // filePath := fmt.Sprintf("./%s",file.Filename)
        filePath := path.Join("./", file.Filename)
        c.SaveUploadedFile(file, filePath)
        c.JSON(http.StatusOK, gin.H{
            "filePath": filePath,
        })
    })
    r.Run(":8082")
}

多檔案上傳

func main() {
    router := gin.Default()
    // 處理multipart forms提交檔案時預設的記憶體限制是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重定向

func main() {
    r := gin.Default()
    //301重定向
    r.GET("/baidu", func(c *gin.Context) {
        c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com")
    })
    r.Run(":8082")
}

路由重定向

func main() {
    r := gin.Default()
    r.GET("/a", func(c *gin.Context) {
        c.Request.URL.Path = "/b"
        r.HandleContext(c)
    })
    r.GET("/b", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
        })
    })
    r.Run(":8082")
}

路由

普通路由

r.GET("/index", func(c *gin.Context) {...})
r.POST("/add", func(c *gin.Context) {...})
r.PUT("/update", func(c *gin.Context) {...})
r.DELETE("/delete", func(c *gin.Context) {...})
//全部請求型別
r.Any("/all", func(c *gin.Context) {...})
//示例
func main() {
    r := gin.Default()
    r.Any("/all", func(c *gin.Context) {
        switch c.Request.Method {
        case http.MethodGet:
            c.JSON(http.StatusOK, gin.H{"method": "GET"})
        case http.MethodPost:
            c.JSON(http.StatusOK, gin.H{"method": "POST"})
        }
    })
    r.Run(":8082")
}

沒有配置處理函式的路由:404

r.NoRoute(func(c *gin.Context) {
        c.HTML(http.StatusNotFound, "views/404.html", nil)
})

路由組r.Group

擁有共同字首的路由為一個路由組,路由組支援巢狀
func main() {
    r := gin.Default()
    userGroup := r.Group("/user")
    {
        userGroup.GET("/index1", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"route": "/user/index1"})
        })
        userGroup.GET("/index2", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"route": "/user/index2"})
        })
        subGroup := userGroup.Group("/sub")
        {
            subGroup.GET("/index3", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{"route": "/user/sub/index3"})
            })
        }
    }
    r.Run(":8082")
}

中介軟體

定義中介軟體

Gin的中介軟體必須是gin.HandlerFunc型別
func m1() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next() // 呼叫該請求的剩餘處理程式
        // c.Abort() // 不呼叫該請求的剩餘處理程式
        cost := time.Since(start)
        fmt.Println("Cost:", cost)
    }
}

註冊中介軟體

全域性註冊

func main() {
    r := gin.Default()
    r.Use(m1())
    r.GET("/index", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
        })
    })
    r.Run(":8082")
}

單獨註冊

func m2() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Abort() // 不呼叫該請求的剩餘處理程式
    }
}

func main() {
    r := gin.Default()
    r.Use(m1())
    r.GET("/index", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
        })
    })
    r.GET("/index2", m2(), func(c *gin.Context) {
      //以下內容不會輸出
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
        })
    })
    r.Run(":8082")
}

元件註冊

shopGroup := r.Group("/shop", m1())
//或shopGroup.Use(m1())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

註冊多個

r.Use(m1, m2, m3(false))

注意事項

gin預設中介軟體

gin.Default()預設使用了LoggerRecovery中介軟體,其中:

  • Logger中介軟體將日誌寫入gin.DefaultWriter,即使配置了GIN_MODE=release
  • Recovery中介軟體會recover任何panic。如果有panic的話,會寫入500響應碼。

如果不想使用上面兩個預設的中介軟體,可以使用gin.New()新建一個沒有任何預設中介軟體的路由。

gin中介軟體中使用goroutine

當在中介軟體或handler中啟動新的goroutine時,不能使用原始的上下文(c *gin.Context),必須使用其只讀副本(c.Copy())。(否則執行中被修改非常不安全)

go funcXX(c.Copy())

多埠執行多個服務

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或者自行開啟兩個goroutine分別啟動兩個服務
    g.Go(func() error {
        return server01.ListenAndServe()
    })

    g.Go(func() error {
        return server02.ListenAndServe()
    })

    if err := g.Wait(); err != nil {
        log.Fatal(err)
    }
}

參考資料

Gin中文文件:https://gin-gonic.com/zh-cn/d...
基於gin框架和gorm的web開發實戰:https://www.bilibili.com/vide...
相關部落格:https://www.liwenzhou.com/pos...

相關文章