支持各种方法的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是已经添加了Logger和Recovery中间件的,而使用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页面。
使用LoadtHTMLGlob和LoadHTMLFiles进行渲染。
例如:
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)除此之外,还有manners,graceful,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
}