Gin
Gin 是一个 Go 语言写的 Web 框架。
1 安装
go get -u github.com/gin-gonic/gin2 HelloWorld
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func helloWorld(context *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "world",
})
}
func main() {
router := gin.Default()
router.GET("/hello", helloWorld)
router.Run("localhost:8000")
}3 路由分组
假设现在有这些请求路径:/goods/list、goods/add、goods/del,在不使用路由分组的情况下,通常会这样写:
router := gin.Default()
router.GET("/goods/list", goodsList)
router.POST("/goods/add", addGoods)
router.POST("/goods/del", delGoods)这些路径都包含 /goods,可以通过路由分组进行管理:
router := gin.Default()
goodsGroup := router.Group("/goods")
goodsGroup.GET("/list", goodsList)
goodsGroup.POST("/add", addGoods)
goodsGroup.POST("/del", delGoods)4 获取 URL 上的变量
假设现在需要通过 /goods/[商品ID] 访问对应商品的详情,通过如下方法可以识别 URL 中的 ID 变量:
router := gin.Default()
router.GET("/goods/:id", func(context *gin.Context) {
id := context.Param("id")
context.JSON(http.StatusOK, gin.H{
"id": id,
})
})假设现在想通过 /goods/[商品ID]/[动作] 来对商品进行操作,可以这样做:
router.GET("/goods/:id/:action", func(context *gin.Context) {
id := context.Param("id")
action := context.Param("action")
context.JSON(http.StatusOK, gin.H{
"id": id,
"action": action,
})
})### 请求
GET http://localhost:8000/goods/123/delete
### 响应
{
"action": "delete",
"id": "123"
}通过 /goods/:id/*action 也能实现,但是和 /:action 有些区别:
router.GET("/goods/:id/*action", func(context *gin.Context) {
id := context.Param("id")
action := context.Param("action")
context.JSON(http.StatusOK, gin.H{
"id": id,
"action": action,
})
})### 请求
GET http://localhost:8000/goods/123/delete
### 响应
{
"action": "/delete",
"id": "123"
}
### 请求
GET http://localhost:8000/goods/123/delete/test
### 响应
{
"action": "/delete/test",
"id": "123"
}
### 请求
GET http://localhost:8000/goods/123
### 响应
{
"action": "/",
"id": "123"
}
### 请求
GET http://localhost:8000/goods/123/
### 响应
{
"action": "/",
"id": "123"
}还可以直接绑定 Uri 到一个结构体:
type Goods struct {
Id int `uri:"id" binding:"required"`
Name string `uri:"name" binding:"required"`
}
func main() {
router := gin.Default()
router.GET("/goods/:id/:name", func(context *gin.Context) {
var goods Goods
if err := context.ShouldBindUri(&goods); err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusOK, gin.H{
"id": goods.Id,
"name": goods.Name,
})
})
router.Run("localhost:8000")
}GET http://localhost:8000/goods/123/abc
# 响应
{
"id": 123,
"name": "abc"
}5 获取表单信息
5.1 Get 请求
func main() {
router := gin.Default()
router.GET("/hello", hello)
router.Run("localhost:8000")
}
func hello(context *gin.Context) {
// 如果参数没取到,则使用空字符串""
lang := context.Query("lang")
// 如果参数没取到,则使用默认值"Gin"
framework := context.DefaultQuery("framework", "Gin")
context.JSON(http.StatusOK, gin.H{
"lang": lang,
"framework": framework,
})
}测试:
GET http://localhost:8000/hello
# 响应
{
"framework": "Gin",
"lang": ""
}GET http://localhost:8000/hello?lang=Java&framework=Spring
# 响应
{
"framework": "Spring",
"lang": "Java"
}5.2 Post 请求
func main() {
router := gin.Default()
router.GET("/hello", hello)
router.POST("/hello", hello)
router.Run("localhost:8000")
}
func hello(context *gin.Context) {
lang := context.Query("lang")
lang := context.PostForm("lang")
framework := context.DefaultQuery("framework", "Gin")
framework := context.DefaultPostForm("framework", "Gin")
context.JSON(http.StatusOK, gin.H{
"lang": lang,
"framework": framework,
})
}测试:
### 请求
POST http://localhost:8000/hello
Content-Type: application/x-www-form-urlencoded
lang = Go &
framework = Gin
# 响应
{
"framework": "Gin",
"lang": "Go"
}6 Protobuf 渲染
定义 Protobuf 消息:
syntax = "proto3";
option go_package = '.;proto';
message Teacher {
string name = 1;
repeated string courses = 2;
}生成 Go 包,在程序中导入使用:
func main() {
router := gin.Default()
router.GET("/hello", hello)
router.Run("localhost:8000")
}
func hello(context *gin.Context) {
user := &proto.Teacher{
Name: "zhang",
Courses: []string{"Gin", "GoLang"},
}
context.ProtoBuf(http.StatusOK, user)
}7 表单验证
type SignUpInfo struct {
Username string `json:"username" binding:"required,min=3,max=20"` // 必传字段,要求 3 <= length <= 20
Password string `json:"password" binding:"required,min=8,max=20"` // 必传字段,要求 3 <= length <= 20
RePassword string `json:"rePassword" binding:"required,eqfield=Password"` // 必传字段,要求和 password 字段一致
Email string `json:"email" binding:"required,email"` // 必传字段,要求符合邮箱格式
Age uint8 `json:"age" binding:"gte=0,lte=120"` // 要求 0 <= age <= 120
}
func main() {
router := gin.Default()
router.POST("/signUp", signUp)
router.Run("localhost:8000")
}
func signUp(context *gin.Context) {
var info SignUpInfo
if err := context.ShouldBindJSON(&info); err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusOK, gin.H{"msg": "注册成功"})
}验证不通过时,返回的信息是英文的,如下所示:
{
"error": "
Key: 'SignUpInfo.Username' Error:Field validation for 'Username' failed on the 'min' tag\n
Key: 'SignUpInfo.RePassword' Error:Field validation for 'RePassword' failed on the 'eqfield' tag\n
Key: 'SignUpInfo.Email' Error:Field validation for 'Email' failed on the 'email' tag\n
Key: 'SignUpInfo.Age' Error:Field validation for 'Age' failed on the 'lte' tag
"
}可将错误信息转成中文:
package main
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
)
func InitTrans(locale string) (err error) {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
zhT := zh.New()
enT := en.New()
uni := ut.New(zhT, zhT, enT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return errors.New("translator not found")
}
switch locale {
case "zh":
if err := zh_translations.RegisterDefaultTranslations(v, trans); err != nil {
return err
}
case "en":
if err := en_translations.RegisterDefaultTranslations(v, trans); err != nil {
return err
}
}
}
return nil
}
var trans ut.Translator
func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println(err)
}
router := gin.Default()
router.POST("/signUp", signUp)
router.Run("localhost:8000")
}
func signUp(context *gin.Context) {
var info SignUpInfo
if err := context.ShouldBindJSON(&info); err != nil {
errs, _ := err.(validator.ValidationErrors)
context.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
return
}
context.JSON(http.StatusOK, gin.H{"msg": "注册成功"})
}此时,错误信息将转为中文:
{
"error": {
"SignUpInfo.Age": "Age必须小于或等于120",
"SignUpInfo.Email": "Email必须是一个有效的邮箱",
"SignUpInfo.RePassword": "RePassword必须等于Password",
"SignUpInfo.Username": "Username长度必须至少为3个字符"
}
}8 中间件
func MyLogger() gin.HandlerFunc {
return func(context *gin.Context) {
t := time.Now()
// context.Abort() 终止请求
context.Next() // 处理请求
latency := time.Since(t)
log.Print(latency)
}
}
func main() {
router := gin.New()
router.Use(gin.Logger()) // 全局中间件
router.GET("/signUp", MyLogger(), signUp) // 路由中间件
router.Run("localhost:8000")
}中间件后续逻辑的执行终止,必须使用 context.Abort(),直接 return 无法阻止执行:
func MyLogger() gin.HandlerFunc {
return func(context *gin.Context) {
return // 后续逻辑依旧会被执行
context.Next()
}
}从添加中间件的 Use 函数源码上看:
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}本质上是把中间件追加到了 group.Handlers 切片的后面。
而在 GET/POST 函数内部执行了一个 combineHandlers 函数:
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}这里将我们处理请求的函数和之前的 Handlers 拼在了一起。
而 context.Next() 的调用过程如下:
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}如果在自定义中间件里直接 return 了,只代表当前中间件的逻辑结束了,Handlers 中后续的函数仍然会依次执行。
从 context.Next() 的源码逻辑可以看出,真正决定 Handlers 中的函数调用的是 c.index。而 context.Abort() 的作用正是修改这个变量:
const abortIndex int8 = math.MaxInt8 >> 1
func (c *Context) Abort() {
c.index = abortIndex
}9 优雅退出
关闭程序的时候可能有请求还没有处理完,此时处理过程就会被迫中断。优雅退出其实就是在程序关闭时,不暴力关闭,而是要等待进程中的逻辑处理完成后,才关闭。
package main
import (
"context"
"errors"
"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.Handler(),
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && !errors.Is(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, 1)
// kill (no params) by default sends syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be caught, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Println("Server Shutdown:", err)
}
log.Println("Server exiting")
}