支持各种方法的API

无论是GET,POST,PUT,PATCH,DELETE,OPTIONS等等方法,可以为一个API指定任意一个方法提供服务。例如

func main() {
	// Creates a gin router with default middleware:
	// logger and recovery (crash-free) middleware
	router := gin.Default()
 
	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)
 
	// By default it serves on :8080 unless a
	// PORT environment variable was defined.
	router.Run()
	// router.Run(":3000") for a hard coded port
}

路径参数

在uri的路径当中可以携带参数。

  • 使用:在路径中可以表示参数,而且必须存在这个参数。使用Param来读取路径参数。
// 只能匹配形如/user/john的url,不能匹配/user/john,/user/等形式
router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")
		c.String(http.StatusOK, "Hello %s", name)
	})
  • 使用*也可以表示参数,但是这个参数可以存在也可以不存在,同样使用Param读取
// 可以匹配/user/john/,也可以匹配/user/john/send
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)
	})

精确度高的路由优先匹配,对于/user/john/,如果存在/user/:name那么就优先匹配这个路由,不存在的话再考虑匹配/user/:name/*action这个路由。

==对于url来说末尾的/是可以看作不存在的,但是对于路由却不可以==。也就是说,url中的字符不能缺失路由中的字符。

  • 在上下文中记录者当前路径匹配的路由,所以我们可以在回调函数中添加一些信息,来查看匹配的是哪个路由
router.POST("/user/:name/*action", func(c *gin.Context) {
        b := c.FullPath() == "/user/:name/*action" // true
        c.String(http.StatusOK, "%t", b)
    })

对于携带参数的路由来说,精确路由都要优先于它进行匹配

查询参数

对于形如/welcome?firstname=Jane&lastname=Doe的携带查询参数的url,可以使用底层的request对象来解析。

router.GET("/welcome", func(c *gin.Context) {
	// 获取不到会有一个默认值
	firstname := c.DefaultQuery("firstname", "Guest")
	// 是c.Request.URL.Query().Get("lastname")的简写
	lastname := c.Query("lastname")	
})

Multipart/Urlencoded 表单

提交的表单信息都可以使用*PostForm的函数来读取

router.POST("/post_form", func(c *gin.Context) {
	message := c.PostForm("message")
	nick := c.DefaultPostForm("nick", "anonymous")
	c.JSON(200, gin.H{
		"status": "posted",
		"message": message,
		"nick": nick,
	})
})

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

在前面对应章节所用解析函数的后面加上Map后缀即可。

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]

文件上传

单个文件

使用FormFile解析出单个文件的数据。然后调用SaveUploadedFile将文件保存到指定路径。

func main() {
	router := gin.Default()
	// Set a lower memory limit for multipart forms (default is 32 MiB)
	router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// Single file
		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")
}

多个文件

文件是以multipart/form-data的格式提交的,可以设置MaxMultipartMemory来限制其能够使用的最大内存。 通过MultipartForm来解析出多个文件的数据

func main() {
	router := gin.Default()
	// Set a lower memory limit for multipart forms (default is 32 MiB)
	router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// Multipart form
		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")
}

自定义中间件

前面使用的示例中,gin.Default是已经添加了LoggerRecovery中间件的,而使用gin.New()创建出来的是空白没有任何中间件的,而我们可以在这个空白上定制自己想要的中间件。

r := gin.New()

有全局使用的中间件,为单个路由使用的中间件,为分组路由使用的中间件。

全局中间件

//Logger中间件将日志写到gin.DefaultWriter,默认为os.Stdout
r.Use(gin.Logger())
// Recovery中间件从任何panics中恢复并写500错误
r.Use(gin.Recovery())

为单个路由设置中间件

r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

为分组路由设置中间件

例如自定义认证中间件

authorized := r.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)
}

自定义recovery行为

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)
	}))

自定义日志中间件

  • 将日志写入文件 默认的日志中间件将日志写到gin.DefaultWriter,这个默认值是os.Stdout,所以只要修改这个值为其它文件,就可以将日志写到日志文件中。
// 禁用控制台的彩色打印,因为不使用控制台
gin.DisableConsoleColor()
// Logging to a file.
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
  • 自定义日志格式
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
 
		// your custom format
		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,
		)
	}))

LoggerWithFormatter也是与Logger一样写入到gin.DefaultWriter中的,所以也可以以同样的方式写入到文件。

  • 控制终端日志输出颜色 默认会根据检测到的终端,将日志加上颜色。可以使用下面的方式关闭使用
gin.DisableConsoleColor()

也可以使用下面的方式强制使用

gin.ForceConsoleColor()

模型绑定和校验

通过模型绑定,可以直接将请求体绑定到一个类型上。支持JSON,XML,YAML和标准的表单键值对。

需要在需要绑定的类型的所有字段上设置对应的绑定标签。例如绑定JSON,就要设置json:"fieldname"

Gin使用go-playground/validator/v10来做校验。

Gin提供了两组用于绑定的函数集合:

  • Type - Must bind
    • 方法:Bind,BindJSON,BindXML,BindQuery,BindYAML,BindHeader
    • 行为:这些方法底层都使用了MustBindWith,如果绑定出错,会立即中断请求,并返回400错误。并且响应码和文本格式都不能自己设置,如果需要自己设置,那么就使用下面的ShouldBind
  • Type - Should Bind
    • 方法:ShouldBind,ShouldBindJSON,ShouldBindXML,ShouldBindQuery,ShouldBindYAML,ShouldBindHeader
    • 行为:底层使用ShouldBindWith,如果绑定出错,会返回一个错误,用户可以自己决定如何处理错误。 Gin通过Content-Type头来推断绑定器。如果有一些字段是必须的,标签中可以添加binding:"required",这样如果在绑定的时候是空值,就会返回错误。这就是一个校验。

一个用于绑定的类型如下:

// Binding from JSON
type Login struct {
	User     string `form:"user" json:"user" xml:"user"  binding:"required"`
	Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

一个JSON格式请求体的绑定如下:

router.POST("/loginJSON", func(c *gin.Context) {
	var json Login
	// 根据返回的错误是否为空来判断绑定是否成功
	if err := c.ShouldBindJSON(&json); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
 
	if json.User != "manu" || json.Password != "123" {
		c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
		return
	}
 
	c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

只绑定query参数

ShouldBindQuery函数只会绑定query参数而不会绑定post数据。

绑定query参数或者Post数据

ShouldBind两个数据都可以绑定。

  • 如果是GET方法,那么使用form标签来绑定query参数
  • 如果是POST方法,先根据Content-Type来确定是否使用JSON或者XML,然后才是form

绑定uri

一个uri中可能有多个路径参数,它们也可以使用一个类型来绑定。使用uri标签。

package main
 
import "github.com/gin-gonic/gin"
 
type Person struct {
	ID string `uri:"id" binding:"required,uuid"`
	Name string `uri:"name" binding:"required"`
}
 
func main() {
	route := gin.Default()
	route.GET("/:name/:id", func(c *gin.Context) {
		var person Person
		if err := c.ShouldBindUri(&person); err != nil {
			c.JSON(400, gin.H{"msg": err.Error()})
			return
		}
		c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
	})
	route.Run(":8088")
}

绑定Header

HTTP的请求头中也可以携带键值对等数据。同样可以绑定到一个类型中,使用ShouldBindHeader方法,使用header标签。 类型如下:

type testHeader struct {
	Rate   int    `header:"Rate"`
	Domain string `header:"Domain"`
}

用法如下:

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		h := testHeader{}
 
		if err := c.ShouldBindHeader(&h); err != nil {
			c.JSON(200, err)
		}
 
		fmt.Printf("%#v\n", h)
		c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
	})
 
	r.Run()
}

绑定html复选框

因为复选框有多个值,所以与其它的不同就是多了一个中括号来表示是一个数组。

type myForm struct {
    Colors []string `form:"colors[]"`
}

调用ShouldBind绑定值

func formHandler(c *gin.Context) {
    var fakeForm myForm
    c.ShouldBind(&fakeForm)
    c.JSON(200, gin.H{"color": fakeForm.Colors})
}

Multipart/Urlencoded绑定

对于上传的文件,要绑定的类型中的字段需要设置为multipart.FileHeader类型,这样就可以使用ShouldBind就可以绑定了。

type ProfileForm struct {
	Name   string                `form:"name" binding:"required"`
	Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
 
	// 对于多文件,使用[]
	// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
}

绑定:

func main() {
	router := gin.Default()
	router.POST("/profile", func(c *gin.Context) {
		// you can bind multipart form with explicit binding declaration:
		// c.ShouldBindWith(&form, binding.Form)
		// or you can simply use autobinding with ShouldBind method:
		var form ProfileForm
		// in this case proper binding will be automatically selected
		if err := c.ShouldBind(&form); err != nil {
			c.String(http.StatusBadRequest, "bad request")
			return
		}
 
		err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
		if err != nil {
			c.String(http.StatusInternalServerError, "unknown error")
			return
		}
 
		// db.Save(&form)
 
		c.String(http.StatusOK, "ok")
	})
	router.Run(":8080")
}

自定义校验器

在绑定的类型中的标签的binding中指定的就是校验器,前面提到required就是一个校验非空的校验器。 给定一个类型:

type Booking struct {
	CheckIn  time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
	CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}

可以看到其CheckIn字段有一个校验器名为bookabledate。这是一个自定义的校验器,其定义为如下,校验字段是否符合时间格式,并且如果当前时间要晚于输入时间会报错。

var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
	date, ok := fl.Field().Interface().(time.Time)
	if ok {
		today := time.Now()
		if today.After(date) {
			return false
		}
	}
	return true
}

然后注册这个校验器即可

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
	v.RegisterValidation("bookabledate", bookableDate)
}

XML,JSON,YAML和ProtoBuf渲染

使用对应的函数可以将响应数据以对应的格式返回。

// JSON
c.JSON(http.StatusOK, msg)
// XML
c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
// YAML
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
// protobuf,要注意data是protobuf数据类型,会序列化为二进制数据传输
c.ProtoBuf(http.StatusOK, data)
  • SecureJSON,如果给定的数据结构是数组的话,会在响应体前加上”while(1),”
names := []string{"lena", "austin", "foo"}
 
// Will output  :   while(1);["lena","austin","foo"]
c.SecureJSON(http.StatusOK, names)
  • JSONP,如果查询参数的回调存在的话,会在结果前加上回调值
data := gin.H{
	"foo": "bar",
}
 
//callback is x
// Will output  :   x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data)
  • AsciiJSON,结果只包含Ascii,非Ascii字符会转义
data := gin.H{
	"lang": "GO语言",
	"tag":  "<br>",
}
 
// will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
c.AsciiJSON(http.StatusOK, data)
  • PureJSON,默认情况下JSON会将HTML中的特殊字符替换成unicode码,例如<替换成\u003c,如果想保留字面量,就使用PureJSON
r.GET("/purejson", func(c *gin.Context) {
	c.PureJSON(200, gin.H{
		"html": "<b>Hello, world!</b>",
	})
})

文件服务

提供静态文件

在内部有一个http.FileServer,可以提供给定文件系统根路径下的文件。

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")
	router.StaticFileFS("/more_favicon.ico", "more_favicon.ico", http.Dir("my_file_system"))
	
	// Listen and serve on 0.0.0.0:8080
	router.Run(":8080")
}

提供文件中的数据

使用File函数并指定文件路径即可。使用FileFromFS可以从一个文件系统中读取文件。

func main() {
	router := gin.Default()
 
	router.GET("/local/file", func(c *gin.Context) {
		c.File("local/file.go")
	})
 
	var fs http.FileSystem = // ...
	router.GET("/fs/file", func(c *gin.Context) {
		c.FileFromFS("fs/file.go", fs)
	})
}

提供Reader中的数据

使用DataFromReader来获取Reader中的数据

func main() {
	router := gin.Default()
	router.GET("/someDataFromReader", func(c *gin.Context) {
		response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
		if err != nil || response.StatusCode != http.StatusOK {
			c.Status(http.StatusServiceUnavailable)
			return
		}
		reader := response.Body
 		defer reader.Close()
		contentLength := response.ContentLength
		contentType := response.Header.Get("Content-Type")
 
		extraHeaders := map[string]string{
			"Content-Disposition": `attachment; filename="gopher.png"`,
		}
 
		c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
	})
	router.Run(":8080")
}

HTML渲染

为engine指定一个存放模板文件的目录,这样在调用HTML方法的时候,就会将数据传递给模板,然后渲染成为HTML页面。 使用LoadtHTMLGlobLoadHTMLFiles进行渲染。 例如:

func main() {
	router := gin.Default()
	router.LoadHTMLGlob("templates/*")
	//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "Main website",
		})
	})
	router.Run(":8080")
}

也可以通过SetHTMLTemplate设置自定义模板:

import "html/template"
 
func main() {
	router := gin.Default()
	html := template.Must(template.ParseFiles("file1", "file2"))
	router.SetHTMLTemplate(html)
	router.Run(":8080")
}

自定义分隔符:

	r := gin.Default()
	r.Delims("{[{", "}]}")
	r.LoadHTMLGlob("/path/to/templates")

自定义模板函数,例如下面是一个格式化时间的函数

func formatAsDate(t time.Time) string {
    year, month, day := t.Date()
    return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}

将其注册到模板函数中

	router := gin.Default()
    router.Delims("{[{", "}]}")
    router.SetFuncMap(template.FuncMap{
        "formatAsDate": formatAsDate,
    })

那么在使用的模板中传递过去一个时间,并调用模板函数进行处理

	router.LoadHTMLFiles("./testdata/template/raw.tmpl")
 
    router.GET("/raw", func(c *gin.Context) {
        c.HTML(http.StatusOK, "raw.tmpl", gin.H{
            "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
        })
    })

raw.tmpl内容如下:

Date: {[{.now | formatAsDate}]}

最终的渲染结果如下:

Date: 2017/07/01

重定向

让一个HTTP重定向很简单,外部跳转和内部跳转都支持。

// 重定向到外部网站
r.GET("/test", func(c *gin.Context) {
	c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})
 
// 重定向到内部网站
r.POST("/test", func(c *gin.Context) {
	c.Redirect(http.StatusFound, "/foo")
})

使用HandleContext来进行路由重定向也是可以的

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

这两者的不同在于,路由跳转不会更改浏览器中的url

中间件

自定义中间件

中间件返回一个处理函数,在请求处理之前或者之后进行某些操作。关注一下函数签名就可以发现,我们一直设置的路由处理函数其实也是中间件。 这些处理函数形成链式结构,使用Next其实就是执行下一个处理函数而已。

func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
 
		// Set example variable
		c.Set("example", "12345")
 
		// before request
 
		c.Next()
 
		// after request
		latency := time.Since(t)
		log.Print(latency)
 
		// access the status we are sending
		status := c.Writer.Status()
		log.Println(status)
	}
}

Logger就返回一个处理函数,在这个函数内部,Next之前表示请求处理之前,Next之后表示请求处理完成。

使用BasicAuth中间件

一个使用BasicAuth的列子如下,也就是先进行用户名和密码校验。

// 模拟私有数据
var secrets = gin.H{
	"foo":    gin.H{"email": "[email protected]", "phone": "123433"},
	"austin": gin.H{"email": "[email protected]", "phone": "666"},
	"lena":   gin.H{"email": "[email protected]", "phone": "523443"},
}

使用路由分组,来为需要进行认证的路由设置BasicAuth中间件。

// gin.Accounts是map[string][string]的简写
	authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
		"foo":    "bar",
		"austin": "1234",
		"lena":   "hello2",
		"manu":   "4321",
	}))
	
	authorized.GET("/secrets", func(c *gin.Context) {
		// get user, it was set by the BasicAuth middleware
		user := c.MustGet(gin.AuthUserKey).(string)
		if secret, ok := secrets[user]; ok {
			c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
		} else {
			c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
		}
	})

在中间件中使用goroutine

在中间件中可以启动协程,但是不应该使用原始的上下文,而是使用其拷贝。

	r.GET("/long_async", func(c *gin.Context) {
		// create copy to be used inside the goroutine
		cCp := c.Copy()
		go func() {
			// simulate a long task with time.Sleep(). 5 seconds
			time.Sleep(5 * time.Second)
 
			// note that you are using the copied context "cCp", IMPORTANT
			log.Println("Done! in path " + cCp.Request.URL.Path)
		}()
	})

这样非常方便用来异步执行一些任务。

自定义HTTP配置

因为gin.Engine实现了ServeHTTP方法,所以它实现了http.Handler接口。可以将其看作http.Handler来使用。 比如:

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

或者:

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

启动多个gin服务

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,
	}
 
	g.Go(func() error {
		err := server01.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
		return err
	})
 
	g.Go(func() error {
		err := server02.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
		return err
	})
 
	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}

优雅地关机和重启

您可以使用一些方法来执行优雅的关闭或重新启动。您可以使用专门为其构建的第三方软件包,也可以使用内置软件包的功能和方法手动执行此操作。

  • 第三方包 可以使用fvbook/endless来替代默认的ListenAndServe
router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)

除此之外,还有mannersgraceful,grace包。

  • 手动 对于go1.18或者更新版本,可以使用http.Server内置的Shutdown方法来优雅地关闭。
srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}
	go func() {
		if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
			log.Printf("listen: %s\n", err)
		}
	}()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
 
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server forced to shutdown:", err)
	}

用模板构建一个二进制程序

利用go-assets来构建一个包含模板的二进制程序。

func main() {
	r := gin.New()
 
	t, err := loadTemplate()
	if err != nil {
		panic(err)
	}
	// 设置模板
	r.SetHTMLTemplate(t)
 
	r.GET("/", func(c *gin.Context) {
		c.HTML(http.StatusOK, "/html/index.tmpl",nil)
	})
	r.Run(":8080")
}
 
// loadTemplate loads templates embedded by go-assets-builder
func loadTemplate() (*template.Template, error) {
	t := template.New("")
	// 加载以.tmpl结尾的文件并返回解析的模板
	for name, file := range Assets.Files {
		defer file.Close()
		if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
			continue
		}
		h, err := ioutil.ReadAll(file)
		if err != nil {
			return nil, err
		}
		t, err = t.New(name).Parse(string(h))
		if err != nil {
			return nil, err
		}
	}
	return t, nil
}

tags: Web框架 gin