Gin Web框架 | 青训营笔记

158 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

Gin

简介

Golang语言编写的 HTTP Web 框架,特点就是性能好。

使用

创建 Gin Engine

API

func Default() *Engine

创建一个 Gin Engine

示例

router := gin.Default()

处理 HTTP 请求

API

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes

// 其余类似。

示例

    router.GET("/someGet", getting)
    router.POST("/somePost", posting)
    router.PUT("/somePut", putting)
    router.DELETE("/someDelete", deleting)
    router.PATCH("/somePatch", patching)
    router.HEAD("/someHead", head)
    router.OPTIONS("/someOptions", options)

返回响应数据

返回 JSON 格式

API

func (c *Context) JSON(code int, obj any)

JSON 函数将给定的结构体序列化为 JSON 字符串 到 response body 中返回。它还将 Content-Type 设置为“application/json”。

返回 字符串

API

func (c *Context) String(code int, format string, values ...any)

String writes the given string into the response body. String 函数将给定的字符串写入 response body 中返回。

启动 Web 服务

API

func (engine *Engine) Run(addr ...string) (err error)

示例

router.Run()

常见 Web 场景

路径匹配问题(主要是路径与路径传值)

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

        // 这个 handler 可以匹配 /user/john 但是不能匹配 /user 或 /user
	router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")
		c.String(http.StatusOK, "Hello %s", name)
	})

        // 这个 handler 可以匹配 /user/john 和 /user/john/send
        // 如果没有其他 Handler 匹配 /user/john, 它将重定向到 /user/john/
        
	router.GET("/user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is " + action
		c.String(http.StatusOK, message)
	})

        // 对于每个匹配的请求,Context 参数都包含请求路径定义(可以通过 Context 获取请求路径)。
	router.POST("/user/:name/*action", func(c *gin.Context) {
		b := c.FullPath() == "/user/:name/*action" // true
		c.String(http.StatusOK, "%t", b)
	})
        
        // 这个 handler 将添加对请求路径为 /user/groups 的请求的处理
        // 不论 handler 定义的顺序如何,明确指明路径的 Handler 在 参数Hanler 前执行
        // 也就是以 /user/grups 开头的路由不会被解析为 /user/:name/... 
	router.GET("/user/groups", func(c *gin.Context) {
		c.String(http.StatusOK, "The available groups are [...]")
	})

	router.Run(":8080")
}

查询字符串

API

Query

func (c *Context) Query(key string) (value string)

如果查询字符串参数存在,返回他的值,否则,返回一个空串。

    GET /path?id=1234&name=Manu&value=
	   c.Query("id") == "1234"
	   c.Query("name") == "Manu"
	   c.Query("value") == ""
	   c.Query("wtf") == ""

DefaultQuery

func (c *Context) DefaultQuery(key, defaultValue string) string

如果查询的字符串参数存在,返回他的值,否则,返回默认值。

GET /?name=Manu&lastname=
c.DefaultQuery("name", "unknown") == "Manu"
c.DefaultQuery("id", "none") == "none"
c.DefaultQuery("lastname", "none") == ""

示例

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

        // 查询字符串通过已有的底层请求对象解析。
        // 这个 Handler 会处理该 URL Mapping: /welcome?firstname=Jan2&lastname=Doe
	router.GET("/welcome", func(c *gin.Context) {
		firstname := c.DefaultQuery("firstname", "Guest")
                // c.Request.URL.Query().Get("lastname") 的简写形式
		lastname := c.Query("lastname") 
		c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
	})
	router.Run(":8080")
}

表单提交

API

PostForm

func (c *Context) PostForm(key string) (value string)

如果key存在,PostForm 从一个 POST urlencoded form 或者 multipart form 中返回指定的key,否则返回一个空字符串("")

DefaultPostForm

func (c *Context) DefaultPostForm(key, defaultValue string) string

如果key存在,PostForm 从一个 POST urlencoded form 或者 multipart form 中返回指定的key,否则返回指定的字符串。

示例

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

	router.POST("/form_post", func(c *gin.Context) {
		message := c.PostForm("message")
		nick := c.DefaultPostForm("nick", "anonymous")

		c.JSON(http.StatusOK, gin.H{
			"status":  "posted",
			"message": message,
			"nick":    nick,
		})
	})
	router.Run(":8080")
}

通过查询字符串 / post 表单发送 Map

POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou
func main() {
	router := gin.Default()

	router.POST("/post", func(c *gin.Context) {

		ids := c.QueryMap("ids")
		names := c.PostFormMap("names")

		fmt.Printf("ids: %v; names: %v", ids, names)
	})
	router.Run(":8080")
}
ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]

文件上传

单文件上传

API

func (c *Context) FormFile(name string) (*multipart.FileHeader, error)

FormFile 返回提供的表单key 中找到的第一个文件

func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error

上传表单文件到指定的dst。

示例

func main() {
	router := gin.Default()
        // 设置 multipart 表单的最低内存限制(默认为32MiB)
	router.MaxMultipartMemory = 8 << 20  // 8 MiB
        
	router.POST("/upload", func(c *gin.Context) {
		// 单文件
		file, _ := c.FormFile("file")
		log.Println(file.Filename)

		// Upload the file to specific dst.
                // 上传文件到指定终端。
		c.SaveUploadedFile(file, dst)

		c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
	})
	router.Run(":8080")
}

如何 curl

curl -X POST http://localhost:8080/upload \
  -F "file=@/Users/appleboy/test.zip" \
  -H "Content-Type: multipart/form-data"

多文件上传

API

func (c *Context) MultipartForm() (*multipart.Form, error)

MultipartForm is the parsed multipart form, including file uploads.

MultipartForm 是经过解析的 mulitpart,其中包括上传文件。

示例

func main() {
	router := gin.Default()
        // 设置 multipart 表单的最低内存限制(默认为32MiB)
        
	router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// Multipart 表单
		form, _ := c.MultipartForm()
		files := form.File["upload[]"]

		for _, file := range files {
			log.Println(file.Filename)
                        
                        // Upload the file to specific dst.
                        // 上传文件到指定终端。
			c.SaveUploadedFile(file, dst)
		}
		c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
	})
	router.Run(":8080")
}

如何 curl

curl -X POST http://localhost:8080/upload \
  -F "upload[]=@/Users/appleboy/test1.zip" \
  -F "upload[]=@/Users/appleboy/test2.zip" \
  -H "Content-Type: multipart/form-data"

路由分组

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

	// Simple group: v1
	v1 := router.Group("/v1")
	{
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}

	// Simple group: v2
	v2 := router.Group("/v2")
	{
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}

	router.Run(":8080")
}

路由分组后,每组 Router 访问都要加组名作为前缀来区分。

例如 上述 /v1/login/v2/login 是两个对不同 URL 路径的 Router。

中间件

创建一个没有默认中间件的 Gin Router

API

func New() *Engine

返回一个新的空白 Engine 实例,不携带任何中间件。

示例

// 使用
r := gin.New()

// 代替

// 默认的创建方式已经附加了 logger 日志中间件和 Recovery 崩溃恢复中间件。
r := gin.Default()

在无默认中间件的 Gin Router 上附加中间件

API

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes

用来将全局中间件附加到 Router (Handler)上,附加的中间件会在每个请求的处理程序链(handler chains)中。

示例

func main() {
        // 创建一个没有默认中间件的 Gin Engine
	r := gin.New()

        // 全局中间件
        // 日志中间件将写日志到到 gin.DefaultWriter,即使设置 GIN_MODE=release。
        // 默认 gin.DefaultWriter = os.Stdout
	r.Use(gin.Logger())

        // Recovery 中间件负责从任一 panics 中恢复,并在 panics 发生时写入 500 响应码
	r.Use(gin.Recovery())

        // 对于每个路由中间件,你能随便添加。
	r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

	// Authorization group
	// authorized := r.Group("/", AuthRequired())
	// 与下面一摸一样
	authorized := r.Group("/")
        
	// per group middleware! in this case we use the custom created
	// AuthRequired() middleware just in the "authorized" group.
        // 这种创建方式将 AuthRequired() 中间件加入了 "authorized" group。
	authorized.Use(AuthRequired())
	{
		authorized.POST("/login", loginEndpoint)
		authorized.POST("/submit", submitEndpoint)
		authorized.POST("/read", readEndpoint)

		// nested group
		testing := authorized.Group("testing")
		// visit 0.0.0.0:8080/testing/analytics
		testing.GET("/analytics", analyticsEndpoint)
	}

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}

自定义 Recovery 中间件行为

func main() {
	// 创建一个没有默认中间件的 Gin Engine
	r := gin.New()

        // 全局中间件
        // 日志中间件将写日志到到 gin.DefaultWriter,即使设置 GIN_MODE=release。
        // 默认 gin.DefaultWriter = os.Stdout
	r.Use(gin.Logger())
        
        // Recovery 中间件负责从任一 panics 中恢复,并在 panics 发生时写入 500 响应码
	r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
		if err, ok := recovered.(string); ok {
			c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
		}
		c.AbortWithStatus(http.StatusInternalServerError)
	}))

	r.GET("/panic", func(c *gin.Context) {
		// panic with a string -- the custom middleware could save this to a database or report it to the user
                // 携带 string 的panic,自定义中间件可以保存这个字符串到数据库或者报告给用户。
		panic("foo")
	})

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

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}

日志

日志记录

func main() {
    // 禁用控制台颜色,将日志写入文件时不需要控制台颜色。
    gin.DisableConsoleColor()

    // 记录日志到一个文件
    f, _ := os.Create("gin.log")
    gin.DefaultWriter = io.MultiWriter(f)

    // 如果需要同时将日志写入文件和控制台,请使用以下代码。
    // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
    
    
    router := gin.Default()
    router.GET("/ping", func(c *gin.Context) {
        c.String(http.StatusOK, "pong")
    })

    router.Run(":8080")
}

自定义日志格式

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

	// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
	// By default gin.DefaultWriter = os.Stdout
        // 日志中间件将写日志到到 gin.DefaultWriter。
        // 默认 gin.DefaultWriter = os.Stdout
	router.Use(gin.LoggerWithFormatter(func(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,
		)
	}))
	router.Use(gin.Recovery())

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

	router.Run(":8080")
}

日志格式示例

::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "

重定向

Get 重定向

r.GET("/test", func(c *gin.Context) {
	c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})

Post 重定向

r.POST("/test", func(c *gin.Context) {
	c.Redirect(http.StatusFound, "/foo")
})

Router 重定向

r.GET("/test", func(c *gin.Context) {
    c.Request.URL.Path = "/test2"
    r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"hello": "world"})
})

数据绑定

Get 查询字符串绑定

PostBody 绑定

表单绑定

HTTP 渲染