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
會按照下面的順序解析請求中的資料完成繫結:
- 如果是
GET
請求,只使用Form
繫結引擎(query
)。 - 如果是
POST
請求,首先檢查content-type
是否為JSON
或XML
,然後再使用Form
(form-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()
預設使用了Logger
和Recovery
中介軟體,其中:
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...