gin框架
# gin框架
- gin是一个路由框架, 是对httpRoute的简单封装;
- 主打轻量, 易学, 高性能
# 创建gin项目
使用go mod管理项目包
开启go module
go env -w GO111MODULE=on // Windows export GO111MODULE=on // macOS 或 Linux
1
2
3设置goproxy
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/ // Windows export GOPROXY=https://mirrors.aliyun.com/goproxy/ // macOS 或 Linux
1
2
3下载gin框架
go get -u github.com/gin-gonic/gin //会下载到gopath的pkg文件夹下
1创建项目文件夹,并初始化mod文件
mkdir proDir cd proDir go mod init proDir touch main.go
1
2
3
4加载gin
go run main.go
1
# 启动HTTP服务
main.go
package main import ( "github.com/gin-gonic/gin" ) func main() { //gin.DefaultWriter = os.Stdin // 不再控制台输出日志 router:= gin.Default() router.GET("/ping", func(context *gin.Context) { context.JSON(200,gin.H{ "msg":"pong", }) }) _ = router.Run("0.0.0.0:8080") }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 静态文件
gin对静态文件的支撑有很多种,这里只介绍两种
- 针对文件夹 StaticFS(relativePath, fs)
- 传入url的相对路径和系统文件夹路径(通过http.Dir获取)
- 针对文件 StatiFS(relativePath, filepath)
- 传入url的相对路径和系统文件的路径(直接传入相对路径的字符串)
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { router:= gin.Default() // 静态资源加载(以css,js,img为例) router.StaticFS("/static",http.Dir("static")) router.StaticFile("/favicon.ico","./static/favicon.ico") _ = router.Run("0.0.0.0:8080") }
1
2
3
4
5
6
7
8
9
10
11
12
13
14- 针对文件夹 StaticFS(relativePath, fs)
# Gin路由
# 1. 普通路由
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
2
3
此外,还有一个可以匹配所有请求方法的Any
方法如下:
r.Any("/test", func(c *gin.Context) {...})
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html
页面。
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
2
3
# 2. 路由组
- 可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对
{}
包裹同组的路由,这只是为了看着清晰,你用不用{}
包裹功能上没什么区别。
func main() {
r := gin.Default()
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})
}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
}
r.Run()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 路由组也是支持嵌套的,例如:
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
// 嵌套路由组
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {...})
}
2
3
4
5
6
7
8
9
通常我们将路由分组用在划分业务逻辑或划分API版本时。
# 3. 路由原理
Gin框架中的路由使用的是httprouter (opens new window)这个库。
其基本原理就是构造一个路由地址的前缀树。
# Gin渲染
# 1. HTML渲染
- 首先定义一个存放模板文件的
templates
文件夹,然后在其内部按照业务分别定义一个posts
文件夹和一个users
文件夹。posts/index.html
文件的内容如下:
{{define "posts/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
users/index.html
文件的内容如下:
{{define "users/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>users/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}
2
3
4
5
6
7
8
9
10
11
12
13
14
Gin框架中使用LoadHTMLGlob()
或者LoadHTMLFiles()
方法进行HTML模板渲染。
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")
//r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "posts/index",
})
})
r.GET("users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "users/index",
})
})
r.Run(":8080")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2. 自定义分隔符
- 可以使用自定义分隔
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates")
2
3
# 3. 自定义模板函数
自定义函数一定要在loadHtml之前!!
定义一个不转义相应内容的
safe
模板函数如下:
func main() {
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML{
return template.HTML(str)
},
})
router.LoadHTMLFiles("./index.tmpl")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", "<a href='https://bigox.wiki'>刘沙河的博客</a>")
})
router.Run(":8080")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 在
index.tmpl
中使用定义好的safe
模板函数:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>修改模板引擎的标识符</title>
</head>
<body>
<div>{{ . | safe }}</div>
</body>
</html>
2
3
4
5
6
7
8
9
- 为了让index.tmpl文件有语法显示,我们还需要配置一下
然后我们加入 *.tmpl,保存即可
# 4. 静态文件处理
- 当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用
gin.Static
方法即可。
func main() {
r := gin.Default()
r.Static("/static", "./static")
r.LoadHTMLGlob("templates/**/*")
// ...
r.Run(":8080")
}
2
3
4
5
6
7
# 5. 使用模板继承
Gin框架默认都是使用单模板,如果需要使用
block template
功能,可以通过"github.com/gin-contrib/multitemplate"
库实现,具体示例如下:首先,假设我们项目目录下的templates文件夹下有以下模板文件,其中
home.tmpl
和index.tmpl
继承了base.tmpl
:
templates
├── includes
│ ├── home.tmpl
│ └── index.tmpl
├── layouts
│ └── base.tmpl
└── scripts.tmpl
2
3
4
5
6
7
- 然后我们定义一个
loadTemplates
函数如下:
func loadTemplates(templatesDir string) multitemplate.Renderer {
r := multitemplate.NewRenderer()
layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
if err != nil {
panic(err.Error())
}
includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
if err != nil {
panic(err.Error())
}
// 为layouts/和includes/目录生成 templates map
for _, include := range includes {
layoutCopy := make([]string, len(layouts))
copy(layoutCopy, layouts)
files := append(layoutCopy, include)
r.AddFromFiles(filepath.Base(include), files...)
}
return r
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 我们在
main
函数中
func indexFunc(c *gin.Context){
c.HTML(http.StatusOK, "index.tmpl", nil)
}
func homeFunc(c *gin.Context){
c.HTML(http.StatusOK, "home.tmpl", nil)
}
func main(){
r := gin.Default()
r.HTMLRender = loadTemplates("./templates")
r.GET("/index", indexFunc)
r.GET("/home", homeFunc)
r.Run()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6. 补充文件路径处理
- 关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
func getCurrentPath() string {
if ex, err := os.Executable(); err == nil {
return filepath.Dir(ex)
}
return "./"
}
2
3
4
5
6
# 7. JSON渲染
func main() {
r := gin.Default()
// gin.H 是map[string]interface{}的缩写
r.GET("/someJSON", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
})
r.GET("/moreJSON", func(c *gin.Context) {
// 方法二:使用结构体
var msg struct {
Name string `json:"user"`
Message string
Age int
}
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.JSON(http.StatusOK, msg)
})
r.Run(":8080")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 8. XML渲染
注意需要使用具名的结构体类型。
func main() {
r := gin.Default()
// gin.H 是map[string]interface{}的缩写
r.GET("/someXML", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
})
r.GET("/moreXML", func(c *gin.Context) {
// 方法二:使用结构体
type MessageRecord struct {
Name string
Message string
Age int
}
var msg MessageRecord
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.XML(http.StatusOK, msg)
})
r.Run(":8080")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 9. YMAL渲染
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
})
2
3
# 10. protobuf渲染
r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// protobuf 的具体定义写在 testdata/protoexample 文件中。
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// 请注意,数据在响应中变为二进制数据
// 将输出被 protoexample.Test protobuf 序列化了的数据
c.ProtoBuf(http.StatusOK, data)
})
2
3
4
5
6
7
8
9
10
11
12
# 获取参数
# 1. 获取querystring参数
querystring
指的是URL中?
后面携带的参数,例如:/user/search?username=小王子&address=沙河
。 获取请求的querystring参数的方法如下:
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
// 可以添加默认值
username := c.DefaultQuery("username", "小王子")
//username := c.Query("username")
address := c.Query("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 我们输入对应的URL,就能获取到对应的参数了
http://localhost:9090/web?username=小王子&address=沙河
# 2. 获取form参数
- 请求的数据通过form表单来提交,例如向
/user/search
发送一个POST请求,获取请求数据的方式如下:
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
// DefaultPostForm取不到值时会返回指定的默认值
//username := c.DefaultPostForm("username", "小王子")
username := c.PostForm("username")
address := c.PostForm("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3. 获取path参数
# *必选参数
使用
:param
的语法完成必选参数,例如/user/:ID
,即可匹配/user/42
这个 URI,但不能匹配/user
或/user/
这个 URI,例如:# :可选参数
使用
*param
的语法完成可选参数的定义,例如/user/*ID
可以匹配/user/42
和/user
或/user
的 URI,例如:router.GET("/user/:ID", func(c *gin.Context) { ID := c.Param("ID") })
1
2
3若没有匹配到,则 ID 为空字符串。
# 获取参数
使用 gin.Context 对象的
c.Param("param")
来获取参数值,参见上面的示例。
- 请求的参数通过URL路径传递,例如:
/user/search/小王子/沙河
。 获取请求URL路径中的参数的方式如下。
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4. 参数绑定
- 为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的
Content-Type
识别请求数据类型并利用反射机制自动提取请求中QueryString
、form表单
、JSON
、XML
等参数到结构体中。 下面的示例代码演示了.ShouldBind()
强大的功能,它能够基于请求自动提取JSON
、form表单
和QueryString
类型的数据,并把值绑定到指定的结构体对象。
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func main() {
router := gin.Default()
// 绑定JSON的示例 ({"user": "q1mi", "password": "123456"})
router.POST("/loginJSON", func(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err == nil {
fmt.Printf("login info:%#v\n", login)
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定form表单示例 (user=q1mi&password=123456)
router.POST("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
router.GET("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
ShouldBind
会按照下面的顺序解析请求中的数据完成绑定:
- 如果是
GET
请求,只使用Form
绑定引擎(query
)。 - 如果是
POST
请求,首先检查content-type
是否为JSON
或XML
,然后再使用Form
(form-data
)。
# 文件上传
# 1. 单个文件上传
- 文件上传前端页面代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>上传文件示例</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="f1">
<input type="submit" value="上传">
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
- 后端gin框架部分代码:
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单个文件
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 2. 多个文件上传
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]
for index, file := range files {
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
router.Run()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 重定向
# HTTP重定向
- HTTP 重定向很容易。 内部、外部重定向均支持。
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})
2
3
# 路由重定向
- 路由重定向,使用
HandleContext
:
r.GET("/test", func(c *gin.Context) {
// 指定重定向的URL
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"hello": "world"})
})
2
3
4
5
6
7
8
# Gin中间件
- Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件.
- 中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
# 1. 定义中间件
Gin中的中间件必须是一个
gin.HandlerFunc
类型。例如我们像下面的代码一样定义一个统计请求耗时的中间件。c.Next() //调用该请求的剩余处理程序
c.Abort() // 不调用该请求的剩余处理程序
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
// 调用该请求的剩余处理程序
c.Next()
// 不调用该请求的剩余处理程序
// c.Abort()
// 计算耗时
cost := time.Since(start)
log.Println(cost)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2. 注册中间件
- 在gin框架中,我们可以为每个路由添加任意数量的中间件。
- 中间的这个通过 Abort() 可以阻止执行
# 2.1 为全局路由注册
func main() {
// 新建一个没有任何默认中间件的路由
r := gin.New()
// 注册一个全局中间件
r.Use(StatCost())
r.GET("/test", func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
r.Run()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.2 为某个路由单独注册
// 给/test2路由单独注册中间件(可注册多个)
r.GET("/test2", StatCost(), func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
2
3
4
5
6
7
8
# 2.3 为路由组注册中间件
- 为路由组注册中间件有以下两种写法。
- 写法1:
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
2
3
4
5
- 写法2:
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
2
3
4
5
6
# 3. 中间件注意事项
# gin默认中间件
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码。如果不想使用上面两个默认的中间件,可以使用
gin.New()
新建一个没有任何默认中间件的路由。
# gin中间件中使用goroutine
- 当在中间件或
handler
中启动新的goroutine
时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)。
# gin的优雅退出
package main
import (
"context"
"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,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && 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)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, 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.Fatal("Server Shutdown:", err)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
}
log.Println("Server exiting")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 运行多个服务
我们可以在多个端口启动服务,例如:
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,
}
// 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 中文文档
- https://gin-gonic.com/zh-cn/docs/