Go(Golang)HTTP Web 框架:Gin 介绍以及详细代码示例

原文链接:https://xiets.blog.csdn.net/article/details/144357170

版权声明:原创文章禁止转载

专栏目录:Golang 专栏(总目录)

Gin 是一个用 Go(Golang)编写的 HTTP Web 框架。它采用了类似 Martini 的 API,但性能比 Martini 快 40 倍。

相关网站:

1. 快速入门

安装 Gin:

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

基础示例:

package main

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

func main() {
	r := gin.Default()
	r.GET("/ping", func(ctx *gin.Context) {
		// gin.H 的底层类型是 map[string]any
		ctx.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})
	err := r.Run() // listen and serve on "0.0.0.0:8080" (default)
	if err != nil {
		panic(err)
	}
}

// 浏览器访问: http://localhost:8080/ping

gin.Context 是一个包含了 RequestResponseWriter 等信息的结构体,*gin.Context 实现了 context.Context 接口。

2 响应渲染器

gin.Context 是 Gin 最重要的部分。它允许我们在中间件之间传递变量、管理流程、验证请求的 JSON 并呈现 JSON 响应。请求处理器函数的参数就是一个 gin.Context 实例,形参通常记作 ctx

2.1 JSON/JSON/YAML/ProtoBuf 渲染器

gin.Context 提供了许多方便使用的渲染器方法,它可以把一个 结构体实例 或 map/slice 自动序列化为你所需要的数据类型写入响应 Body,并自动添加对应的 Content-Type 到响应头。

渲染器方法包括:

  • ctx.JSON(code int, resBody)
  • ctx.AsciiJSON(code int, obj any)
  • ctx.YAML(code int, resBody)
  • ctx.TOML(code int, resBody)
  • ctx.XML(code int, resBody)
  • ctx.ProtoBuf(code int, resBody)

以上方法实际内部调用的是一个渲染方法 ctx.Render(code int, r render.Render),通过内置的各种渲染器实现自动序列化,也可以自定义渲染器。

渲染器方法示例:

package main

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

func main() {
	// 创建一个 gin.Engine 实例,其中已附加 gin.Logger() 和 gin.Recovery() 中间件。不使用默认中间件,可以通过 gin.New() 创建实例。
	router := gin.Default()

	type Person struct {
		Name string `json:"name"`
		Age  int    `json:"age"`
	}

	// GET /get_json, 相当于: r.Handle("GET", path, handlers)
	router.GET("/get_json", func(ctx *gin.Context) {
		p := &Person{
			Name: "tom",
			Age:  18,
		}
		// 将给定的结构体序列化为 JSON 写入响应 Body 中,并设置响应头 "Content-Type: application/json; charset=utf-8"
		ctx.JSON(http.StatusOK, p)
		// ctx.AsciiJSON(code int, obj any)		// 非 ASCII 字符以 \u 的形式表示

		// 其他自动序列化对象响应的方法
		// ctx.YAML(code int, obj any)			// Content-Type: application/yaml; charset=utf-8
		// ctx.TOML(code int, obj any)			// Content-Type: application/toml; charset=utf-8
		// ctx.XML(code int, obj any)			// Content-Type: application/xml; charset=utf-8
		// ctx.ProtoBuf(http.StatusOK, resBody) // Content-Type: application/x-protobuf
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

2.2 通用响应数据

ctx.String() 方法用于将字符串写入到 Body,并设置 Content-Type: text/plain; charset=utf-8 响应头。

ctx.Data()ctx.DataFromReader() 方法则用于将任意二进制数据写入响应 Body,并自定义 Content-Type

package main

import (
	"bytes"
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

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

    // GET /get_info
	router.GET("/get_info", func(ctx *gin.Context) {
		resBody := bytes.NewBuffer(nil)
		resBody.WriteString("RemoteIP: " + ctx.RemoteIP() + "\n")
		resBody.WriteString("FullPath: " + ctx.FullPath() + "\n")
		resBody.WriteString(fmt.Sprintf("http.Request: %+v", ctx.Request)) // ctx.Request 是 *http.Request 类型

		// 文本响应:"Content-Type: text/plain; charset=utf-8"
		ctx.String(http.StatusOK, resBody.String())

		// 其他任意数据响应
		// ctx.Data(code int, contentType string, data []byte)
		// ctx.DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

3. 路由参数与自动重定向

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	// 路径参数:路径中带有 name 和 id 两个参数
	router.GET("/user/:name/:id", func(ctx *gin.Context) {
		// 遍历路径中的所有参数
		for _, param := range ctx.Params {
			fmt.Printf("Params: %s=%s\n", param.Key, param.Value)
		}

		// 路径中的指定参数 (参数值均以字符串返回, 如果不存在则返回空字符串)
		fmt.Printf("id=%s\n", ctx.Param("id"))
		fmt.Printf("name=%s\n", ctx.Param("name"))

		// 路径中的参数绑定到结构体中
		type Person struct {
			ID   int64  `uri:"id" binding:"required"`
			Name string `uri:"name" binding:"required"`
		}
		p := &Person{}
		err := ctx.ShouldBindUri(p) // 如果参数值无法解析为字段需要的类型 或者 不符合required约束, 则返回 error
		if err != nil {
			panic(err)
		}
		fmt.Printf("p=%+v\n", p)

		ctx.String(http.StatusOK, "succeed")
	})

	// 路径泛匹配:以 "/home/*" 开头的路径都会匹配到此路由,action 用于接收 "/home" 后面的路径(以"/"开头)
	router.GET("/home/*action", func(ctx *gin.Context) {
		action := ctx.Param("action")
		ctx.String(http.StatusOK, action)
	})

	// 自动重定向:如果注册了 "/hello",但没注册 "/hello/",则后者会自动重定向到前者
	router.GET("/hello", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "/hello")
	})

	// 自动重定向:如果注册了 "/world/",但没注册 "/world",则后者会自动重定向到前者
	router.GET("/world/", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "/world")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

测试访问:

  • http://localhost:8080/user/abc/123
  • http://localhost:8080/home/abc/def/123
  • http://localhost:8080/hello/
  • http://localhost:8080/world

4. Query 参数

*gin.Context 中获取 Query 参数值的相关方法:

  • ctx.Query(key string) (value string):获取指定名称的 Query 参数值,没有则返回空字符串。
  • ctx.GetQuery(key string) (string, bool):获取指定名称的 Query 参数值,返回 (参数值, 是否存在)
  • ctx.DefaultQuery(key, defaultValue string) string:获取指定名称的 Query 参数值,如果不存在则返回 defaultValue

以数组/Map的方式获取 Query 参数值:

  • ctx.QueryArray(key string) (values []string)
  • ctx.GetQueryArray(key string) (values []string, ok bool)

Query 参数绑定到对象:

  • ctx.ShouldBindQuery(obj any) error

Query 参数获取示例:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	router.GET("/get", func(ctx *gin.Context) {
		name := ctx.Query("name") // 参数值自动 UrlDecode
		age := ctx.Query("age")
		fmt.Printf("name=%s, age=%s\n", name, age)

		type Person struct {
			Name string `query:"name"` // 参数值自动 UrlDecode
			Age  int    `query:"age"`
		}
		p := &Person{}
		err := ctx.ShouldBindQuery(p)
		if err != nil {
			panic(err)
		}
		fmt.Printf("p=%+v\n", p)

		ctx.String(http.StatusOK, "succeed")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

访问:http://localhost:8080/get?name=tom&age=18

5. 处理表单提交

获取 Form 表单字段的相关方法:

  • ctx.PostForm(key string) (value string):获取表单中指定字段的值。
  • ctx.GetPostForm(key string) (string, bool):获取表单中指定字段的值,返回 (字段值, 是否存在)
  • ctx.DefaultPostForm(key, defaultValue string) string:获取表单中指定字段的值,如果不存在则返回 defaultValue
  • ctx.PostFormArray(key string) (values []string):以数组的方式获取表单值。
  • ctx.GetPostFormArray(key string) (values []string, ok bool):以数组的方式获取表单值。

上面获取表单字段值的方法,同时支持 application/x-www-form-urlencodedmultipart/form-data 格式的表单。

获取 multipart/form-data 复合表单中的文件字段:

  • ctx.FormFile(name string) (*multipart.FileHeader, error):获取复合表单中的文件字段。
  • ctx.SaveUploadedFile(file *multipart.FileHeader, dst string) error:保存表单文件字段的文件内容到指定路径。
  • ctx.MultipartForm() (*multipart.Form, error):获取解析后的复合表单,包括文件字段。

处理表单提交示例:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	// 为 multipart forms 设置较低的内存限制 (默认是 32 MB)
	router.MaxMultipartMemory = 10 << 20  // 10 MB

	router.POST("/post", func(ctx *gin.Context) {
		// 获取表单字段
		name := ctx.PostForm("name")
		age := ctx.DefaultPostForm("age", "18")
		fmt.Printf("name=%s, age=%s\n", name, age)

		// 获取上传的文件
		file, err := ctx.FormFile("file")
		if err != nil {
			panic(err)
		}
		if file != nil {
			fmt.Printf("file: name=%s, size=%d, MIME=%s\n", file.Filename, file.Size, file.Header["Content-Type"])
			// file.Open() // 打开文件流
			// 保存文件到指定路径
			err = ctx.SaveUploadedFile(file, "./"+file.Filename)
			if err != nil {
				panic(err)
			}
		}

		ctx.String(http.StatusOK, "succeed")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

测试:

curl -F "name=tom" -F "age=18" -F "file=@/path/demo.png" "http://localhost:8080/post"

6. 绑定 Request Body

如果 Request Body 是 JSON、XML、YAML、TOML 格式的内容,Gin 支持直接将其绑定到对象,相关方法:

  • ctx.ShouldBindBodyWithJSON(obj any) error
  • ctx.ShouldBindBodyWithXML(obj any) error
  • ctx.ShouldBindBodyWithYAML(obj any) error
  • ctx.ShouldBindBodyWithTOML(obj any) error
  • ctx.ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error):上面的方法内部调用的是此方法。
  • ctx.ShouldBind(obj any) error:根据请求头中的 Content-Type 字段值自动选择绑定类型,例如:
    • "application/json" --> JSON binding
    • "application/xml" --> XML binding

如果要获取 Request Body 原始数据,可以调用 ctx.GetRawData() ([]byte, error)

绑定 Request Body 示例:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	router.POST("/post", func(ctx *gin.Context) {
		type Person struct {
			Name string `json:"name" yaml:"name" toml:"name" binding:"required"`
			Age  int    `json:"age" yaml:"age" toml:"age" binding:"required"`
		}
		p := &Person{}

		err := ctx.ShouldBind(p) // 自动识别 Request Body 内容类型, 并绑定到对象
		if err != nil {          // 如果不满足 类型 和 required 约束, 则返回 error
			panic(err)
		}
		fmt.Printf("p: %+v\n", p)

		ctx.String(http.StatusOK, "succeed")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

测试:

curl -H "Content-Type: application/json" -d '{"name": "tom", "age": 18}' http://localhost:8080/post

7. 设置响应头、设置/获取Cookie

设置响应头方法:

  • ctx.Header(key, value string):设置(覆盖)响应头,如果 value="" 则移除响应头。

设置/获取 Cookie 方法:

  • ctx.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool):设置 Cookie 到响应头。
  • ctx.Cookie(name string) (string, error):从请求头中获取 Cookie。
package main

import (
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	router.POST("/login", func(ctx *gin.Context) {
		ctx.Header("X-Server", "My Server")

		username := ctx.PostForm("username")
		password := ctx.PostForm("password")
		if username == "tom" && password == "123" {
			ctx.SetCookie("token", "uuid*****", 3600, "/", "localhost", false, true)
			ctx.String(http.StatusOK, "succeed")
		} else {
			ctx.String(http.StatusOK, "failed")
		}
	})

	router.GET("/home", func(ctx *gin.Context) {
		token, err := ctx.Cookie("token")
		if err != nil {
			if errors.Is(err, http.ErrNoCookie) {
				fmt.Println("no token cookie")
			} else {
				panic(err)
			}
		} else {
			fmt.Println("Token:", token)
		}
		ctx.Header("X-Server", "My Server")
		ctx.String(http.StatusOK, "home")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

测试:

curl -v -d "username=tom&password=123" http://localhost:8080/login

curl -v -H "Cookie: token=uuid%2A%2A%2A%2A%2A" http://localhost:8080/home

8. 中间件

中间件注册方法:ctx.Use(middleware ...HandlerFunc) IRoutes

HandlerFunc 是一个函数类型:type HandlerFunc func(*gin.Context)

8.1 自定义中间件

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

// Middleware1 返回一个中间件函数
func Middleware1() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		fmt.Println("Middleware1 Start")

		ctx.Set("name", "tom") // 在 Context 中保存变量

		ctx.Next() // 调用下一个中间件,如果没有更多中间件,则调用 handler

		fmt.Println("Middleware1 End")
	}
}

// Middleware2 一个中间件函数
func Middleware2(ctx *gin.Context) {
	name, _ := ctx.Get("name") // 获取 Context 中保存的变量

	fmt.Println("Middleware2 Start, name:", name)

	ctx.Next() // 调用下一个中间件,如果没有更多中间件,则调用 handler

	fmt.Println("Middleware2 End")
}

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

	// 注册中间件,多个中间件将依次调用
	router.Use(Middleware1())
	router.Use(Middleware2)

	router.GET("/ping", func(ctx *gin.Context) {
		name, _ := ctx.Get("name")
		fmt.Println("GET Handler: name:", name)
		ctx.String(http.StatusOK, "pong")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

访问 http://localhost:8080/ping,输出日志:

Middleware1 Start
Middleware2 Start, name: tom
GET Handler: name: tom
Middleware2 End
Middleware1 End

8.2 日志中间件

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

func LogFormatter(param gin.LogFormatterParams) string {
	// 自定义日志格式
	return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
		param.ClientIP,
		param.TimeStamp.Format(time.RFC1123),
		param.Method,
		param.Path,
		param.Request.Proto,
		param.StatusCode,
		param.Latency,
		param.Request.UserAgent(),
		param.ErrorMessage,
	)
}

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

	// gin.LoggerWithFormatter 中间件会写入日志到 gin.DefaultWriter
	// 默认 gin.DefaultWriter = os.Stdout
	router.Use(gin.LoggerWithFormatter(LogFormatter))

	router.GET("/ping", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "pong")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

8.3 BasicAuth 中间件

package main

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

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

	// 全局注册 BasicAuth 中间件 (也可以在某个 path 注册, 或某个路由组注册)
	router.Use(gin.BasicAuth(gin.Accounts{
		"user1": "pass1", // 账号 {用户名: 密码}
		"user2": "pass2",
	}))

	router.GET("/user", func(ctx *gin.Context) {
		// 获取通过 BasicAuth 中间件认证的用户名,在 gin.BasicAuth 中通过 ctx.Set(gin.AuthUserKey, user) 设置
		user := ctx.MustGet(gin.AuthUserKey).(string)
		ctx.String(http.StatusOK, "hello %s", user)
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

测试访问:

curl -v "http://user1:pass1@localhost:8080/user"

8.4 Gzip 中间件

Gin 官方的 GitHub 中提供了许多中间件,例如:gin-contrib/gzipgin-contrib/corsgin-contrib/sessionsgin-contrib/i18n 等,具体参考 gin-contrib — GitHub

安装 GZIP 中间件:

go get github.com/gin-contrib/gzip

给支持 gzip 压缩的客户端进行 gzip 压缩:

package main

import (
	"github.com/gin-contrib/gzip"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

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

	// 注册 gzip 插件,使用默认的压缩等级,排除 ".pdf" 和 ".mp4" 后缀的资源
	router.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp4"})))

	// gzip.WithExcludedPathsRegexs() // 排除指定的路径规则 (正则匹配)
	// gzip.WithExcludedPaths()       // 排除指定的路径

	router.GET("/ping", func(ctx *gin.Context) {
		text := "pong\n"
		for i := 0; i < 100; i++ {
			text += time.Now().String() + "\n"
		}
		ctx.String(http.StatusOK, text)
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

9. 路由组

package main

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

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

	v1 := router.Group("/v1")
	{
		// v1.Use(...)                     // 路由组内中间件
		v1.GET("/home", v1HomeHander)      // 访问: GET "/v1/home"
		v1.POST("/submit", v1SubmitHander) // 访问: POST "/v1/submit"
	}

	v2 := router.Group("/v1")
	{
		v2.GET("/home", v2HomeHander)      // 访问: GET "/v2/home"
		v2.POST("/submit", v2SubmitHander) // 访问: POST "/v2/submit"
	}

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

10. 重定向与转发

package main

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

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

	router.GET("/hi", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "hi")
	})

	router.GET("/hello1", func(ctx *gin.Context) {
		// 外部重定向(由客户端重定向)
		ctx.Redirect(http.StatusMovedPermanently, "/hi")
	})

	router.GET("/hello2", func(ctx *gin.Context) {
		// 内部重定向(服务端转发)
		ctx.Request.URL.Path = "/hi"
		router.HandleContext(ctx)
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

11. 静态文件服务

package main

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

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

	router.Static("/assets", "./assets")
	router.StaticFS("/more_static", http.Dir("my_file_system"))
	router.StaticFile("/favicon.ico", "./resources/favicon.ico")

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

静态文件服务,支持以下功能:

  • 响应头中会返回 Last-Modified,再次请求时如果携带 If-Modified-Since,如果内容没改变则会返回 304 Not Modified
  • 同时支持 GETHEAD 请求。
  • 支持 Range: bytes=start-end 请求头。

12. 自定义 HTTP 配置

gin.Default()gin.New() 返回的是一个 *gin.Engine 实例,它实现了 gin.IRoutes 路由接口,也是实现了 http.Handler 接口。因此可以直接使用 Go 内置的 net/http 模块把 *gin.Engine 实例作为 http.Handler 启动 HTTP/HTTPS 服务器。

import "net/http"

func main() {
	router := gin.Default()
	http.ListenAndServe(":8080", router)
}

import "net/http"

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

	server := &http.Server{
		Addr:           ":8080",
		Handler:        router,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	server.ListenAndServe()
}

实际上 gin.Default().Run() 内部也是通过调用 http.ListenAndServe() 启动服务。

13. HTTP2 Server Push

package main

import (
	"github.com/gin-gonic/gin"
	"html/template"
	"log"
	"net/http"
)

func main() {
	// 内存中创建一个 HTML 模板,名称为 "index"
	indexHtml, err := template.New("index").Parse(`
		<!DOCTYPE html>
		<html lang="en">
		<head>
			<meta charset="UTF-8">
			<title>Hello</title>
			<script src="/assets/app.js"></script>
		</head>
		<body>
		Hello World
		</body>
		</html>
	`)
	if err != nil {
		panic(err)
	}

	router := gin.Default()

	router.Static("/assets", "./assets")
	router.SetHTMLTemplate(indexHtml)

	router.GET("/", func(ctx *gin.Context) {
		// 主动推送资源
		if pusher := ctx.Writer.Pusher(); pusher != nil {
			if err = pusher.Push("/assets/app.js", nil); err != nil {
				log.Printf("Push Failed: %+v\n", err)
			}
		}
		ctx.HTML(http.StatusOK, "index", gin.H{})
	})

	// 必须是 TLS 服务才支持 HTTP2 Server Push
	err = router.RunTLS(":8080", "cert.pem", "key.pem")
	if err != nil {
		panic(err)
	}
}

14. HTML 渲染器

使用 HTML 渲染器,要先加载 HTML 模板文件,相关方法:

  • engine.LoadHTMLGlob(pattern string):加载由 glob pattern 标识的 HTML 文件,并将结果与 HTML 渲染器相关联。默认文件名为 HTML 文件模板的名称。
  • engine.LoadHTMLFiles(files ...string):加载一段 HTML 文件,并将结果与 HTML 渲染器相关联。默认文件名为 HTML 文件模板的名称。

HTML 渲染器方法:

  • ctx.HTML(code int, name string, obj any):渲染 HTML 模板,name 表示加载的模板名称,obj 是模板中可以使用对象。自动设置 Content-Type: text/html; charset=utf-8 响应头。

14.1 HTML 渲染器简单示例

下面是一个 HTML 模板文件:./templates/hello.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
My name is {{ .name }}, age is {{ .age }}.
</body>
</html>

{{ .name }}{{ .age }} 表示引用对象的属性插值。模板中引用对象属性,默认使用 {{ }} 分隔符,可以通过 router.Delims(left, right string) 自定义设置分隔符,例如:router.Delims("{[{", "}]}")

加载模板,启动 HTTP 服务器:./main.go

package main

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

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

	router.LoadHTMLGlob("./templates/*.html")         // 把 "./templates/" 目录下的所有 html 文件加载为模板,对应的模板名称为文件名
	// router.LoadHTMLFiles("./templates/hello.html") // 加载单个 HTML 文件模板

	// GET /hello
	router.GET("/hello", func(ctx *gin.Context) {
		// 渲染名称为 "hello.html" 的已加载模板,并向模板中传入一个对象
		ctx.HTML(http.StatusOK, "hello.html", gin.H{
			"name": "tom",
			"age":  18,
		})
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

测试访问:

$ curl http://localhost:8080/hello
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
My name is tom, age is 18.
</body>
</html>

14.2 自定义模板名称

LoadHTMLGlob()LoadHTMLFiles() 加载的 HTML 模板,默认使用文件名作为模板名称。

在 HTML 模板文件中通过 {{ define "name" }}{{ end }} 包裹 HTML 自定义 HTML 对应的模板名称。HTML 模板文件,可以命名为 *.tmpl,例如下面一个模板文件:./templates/hello.tmpl

{{ define "hello-tmpl" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
hello-tmpl: My name is {{ .name }}, age is {{ .age }}.
</body>
</html>
{{ end }}

上面模板文件中,模板名称为 hello-tmpl,当加载这个模板文件后,自动注册该模板,由 {{ define "name" }}{{ end }} 中间包裹的内容就是该模板名称对应的 HTML 渲染内容:

package main

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

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

	router.LoadHTMLGlob("./templates/*.tmpl")
	// router.LoadHTMLFiles("./templates/hello.tmpl")

	router.GET("/hello", func(ctx *gin.Context) {
		// 渲染名称为 "hello-tmpl" 的已加载模板,并向模板中传入一个对象
		ctx.HTML(http.StatusOK, "hello-tmpl", gin.H{
			"name": "tom",
			"age":  18,
		})
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

测试访问:

$ curl http://localhost:8080/hello

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
hello-tmpl: My name is tom, age is 18.
</body>
</html>

一个 HTML 模板文件中,可以有多组 {{ define "name" }} ... {{ end }},分别对应多个模板。模板文件本身也会以文件名为名称注册一个模板,其模板内容为 {{ define "name" }} ... {{ end }} 之外的内容。

14.3 自定义模板功能

模板中通过 {{ .attr }} 可以直接引用对象属性,还可以在其中调用一个函数:

package main

import (
	"github.com/gin-gonic/gin"
	"html/template"
	"net/http"
	"time"
)

func FormatAsDate(t time.Time) string {
	return t.Format("2006-01-02 15:04:05")
}

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

	// 设置函数映射
	router.SetFuncMap(template.FuncMap{
		"formatAsDate": FormatAsDate,
	})

	// 加载 HTML 模板
	router.LoadHTMLGlob("./*.tmpl")

	router.GET("/get-date", func(ctx *gin.Context) {
		ctx.HTML(http.StatusOK, "get-date.tmpl", gin.H{
			"now": time.Now(),
		})
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

在模板中调用函数:./get-date.tmpl

Date: {{ .now | formatAsDate }}

结果:

Date: 2025-10-01 15:00:00

15. 其他

15.1 gin.Context 副本

如果要在中间件中使用 Goroutine 执行异步任务,则使用的上下文必须是 gin.Context 的副本:

package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"time"
)

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

	router.GET("/run-async-task", func(ctx *gin.Context) {
		cpCtx := ctx.Copy()
		go func() {
			time.Sleep(5 * time.Second)
			log.Println("Done! in path ", cpCtx.Request.URL.Path)
		}()
		ctx.String(http.StatusOK, "task start")
	})

	err := router.Run(":8080")
	if err != nil {
		panic(err)
	}
}

15.2 优雅地重启或停止

优雅地重启或停止服务,主要是通过接收操作系统发出的 中断信号(os.Interrupt) 或 强制停止信号(os.Kill),然后做收尾工作,再主动关闭服务。

package main

import (
	"context"
	"errors"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"
)

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

	router.GET("/ping", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "pong")
	})

	server := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}

	// 异步启动服务
	go func() {
		err := server.ListenAndServe()
		if err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("listen error: %+v\n", err)
		}
	}()

	// 监听中断信号
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("Interrupt signal received.")

	// 收到中断信号后,主动关闭服务(设置5秒超时时间,超过5秒没有正常关闭服务,则取消上下文)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()              // 如果顺利关闭服务,则提前取消掉上下文
	err := server.Shutdown(ctx) // 主动关闭服务,关闭服务前有5秒钟时间做收尾工作(例如关闭其他关联的服务,把缓存的日志刷到磁盘)
	if err != nil {
		log.Fatalf("Server Shutdown Error: %+v\n", err)
	}
	log.Println("Server Shutdown.")
}

启动服务后,按 Ctrl + C 向进程发出中断信号。

15.3 运行多个服务

一个进程可以运行多个服务,包括 HTTP服务、gRPC服务、Kafka服务 等。

管理多个服务,可以使用 golang.org/x/sync/errgroupsync.WaitGroup

errgroup 包为执行共同任务的子任务 goroutine 组提供了同步、error传播 和 上下文取消功能。

errgroup.Group 内部使用 sync.WaitGroup 实现,但增加了对返回错误的任务的处理。

安装 errgroup

go get -u golang.org/x/sync/errgroup

下面代码中启动两个 HTTP 服务 和 一个等待接收中断信号的服务,并在接收到中断信号时优雅地关闭服务:

package main

import (
	"context"
	"errors"
	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"
)

func main() {
	ctx := context.Background()

	// 返回一个新的 errgroup.Group 和一个从 ctx 派生的关联 Context。
	// 当传递给 group.Go() 的函数第一次返回非 nil error 或 第一次 group.Wait() 返回时(以先发生者为准),返回的 ctx 会被取消
	group, ctx := errgroup.WithContext(ctx)

	// HTTP 服务
	router1 := gin.Default()
	router1.GET("/ping", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "pong")
	})
	server1 := &http.Server{
		Addr:    ":8080",
		Handler: router1,
	}
	// 在新的 goroutine 中启动服务
	group.Go(func() error {
		log.Println("Server1 Start.")
		return server1.ListenAndServe()
	})

	// HTTP 服务
	router2 := gin.Default()
	router2.GET("/ping", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "pong")
	})
	server2 := &http.Server{
		Addr:    ":8081",
		Handler: router1,
	}
	group.Go(func() error {
		log.Println("Server2 Start.")
		return server2.ListenAndServe()
	})

	// 异步接收中断信号的服务
	group.Go(func() error {
		quit := make(chan os.Signal, 1)
		signal.Notify(quit, os.Interrupt)
		<-quit
		log.Println("Interrupt Signal Received.")

		// 收到中断信号后,主动关闭服务(超时5秒则强制取消上下文)
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		// 主动关闭 HTTP 服务
		if err := server1.Shutdown(ctx); err != nil {
			log.Printf("Shutdown Server1 Error: %+v\n", err)
		} else {
			log.Println("Server1 Shutdown.")
		}

		// 主动关闭 HTTP 服务
		if err := server2.Shutdown(ctx); err != nil {
			log.Printf("Shutdown Server2 Error: %+v\n", err)
		} else {
			log.Println("Server2 Shutdown.")
		}

		log.Println("All Server Safely Exited.")
		return nil
	})

	// 等待组内所有 goroutine 结束(如果其中一个 goroutine 返回了 err,则会取消 ctx),
	// 返回的 err 是这一组 goroutine 中第一个返回的非 nil error
	if err := group.Wait(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatalln(err)
	}
	// group.Wait() 内部也是通过调用 sync.WaitGroup 的 Wait() 方法实现的等待

	log.Println("APP EXISTS.")
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢TS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值