Go:gin
目录:
gin简介
gin是一个go语言编写的web框架
安装方式
go get github.com/gin-gonic/gin
使用
gin示例
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
// c.JSON:返回JSON格式的数据
c.JSON(200, gin.H{
"message": "Hello world!",
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run(":22222")
}
请求对应接口
$ curl 127.0.0.1:22222/hello
{"message":"Hello world!"}
获取参数
获取querystring参数
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
username := c.DefaultQuery("username", "why")
address := c.Query("address")
c.JSON(200, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run(":22222")
}
请求对应接口
$ curl '127.0.0.1:22222/user/search?username=wanghongyu&address=chengde'
{"address":"chengde","message":"ok","username":"wanghongyu"}
获取form参数
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
username := c.PostForm("username")
address := c.PostForm("address")
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run(":22222")
}
请求对应接口
$ curl '127.0.0.1:22222/user/search' -d 'username=wanghongyu&address=chengde'
{"address":"chengde","message":"ok","username":"wanghongyu"}
获取path参数
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run(":22222")
}
请求对应接口
$ curl '127.0.0.1:22222/user/search/wanghongyu/chengde'
{"address":"chengde","message":"ok","username":"wanghongyu"}
参数绑定
请求通过基于Content-Type识别数据类型,利用反射机制提取请求中的QueryString
、form
表单、json数据等
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func main() {
r := gin.Default()
// 绑定JSON的示例 ({"user": "why", "password": "123456"})
r.POST("/loginJSON", func(c *gin.Context) {
var login Login
if err := c.ShouldBindJSON(&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()})
}
})
// 绑定form表单示例 (user=why&password=123456)
r.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=why&password=123456)
r.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()})
}
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run(":22222")
}
curl -X POST -H "content-type/application/json" 127.0.0.1:22222/loginJSON -d '{"user": "why", "password": "123456"}'
{"password":"123456","user":"why"}
curl -X POST -H "content-type: application/x-www-form-urlencoded" 127.0.0.1:22222/loginForm -d 'user=why&password=123456'
{"password":"123456","user":"why"}
curl -X GET '127.0.0.1:22222/loginForm?user=why&password=123456'
{"password":"123456","user":"why"}
ShouldBind会按照下面的顺序解析请求中的数据完成绑定
- 如果是GET请求,只使用Form绑定引擎query。
- 如果是POST请求,首先检查content-type是否为JSON或XML,然后再使用Form(form-data)。
文件上传
单个文件上传
前端代码
<!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>
后端代码
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"log"
)
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("/tmp/%s", file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}
多个文件上传
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"log"
)
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()
}
重定向
HTTP重定向
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.whysdomain.com/")
})
路由重定向
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"})
})
路由
普通路由
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
方法不同可以进行不同的路由,还有一个匹配所有请求方法的Any方法
r.Any("/test", func(c *gin.Context) {...})
对于没有路由到的也可以添加处理程序
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
路由组
将相同的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()
}
路由组支持嵌套
路由原理
使用了httprouter库
Gin中间件
在请求中添加钩子,可以做一些公共的逻辑,例如登录认证,权限校验,数据分页,记录耗时等等
定义中间件
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "why") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
// 调用该请求的剩余处理程序
c.Next()
// 不调用该请求的剩余处理程序
// c.Abort()
// 计算耗时
cost := time.Since(start)
log.Println(cost)
}
}
注册中间件
全局注册
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()
}
单个路由注册
// 给/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!",
})
})
路由组注册
有两种写法
第一种:
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
第二种
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
注意事项
默认中间件
gin.Default()使用了Logger和Recovery中间件
- Logger中间件会将日志写入gin.DefaultWriter
- Recover中间件会recover任何panic,并将panic写入500的响应码
如果不想使用可以用gin.New()新建一个没有任何中间件的路由
中间件中使用goroutine
在handler或者中间件启动新的goroutine的时候,不能使用原始的上下文(c *gin.Context),需要使用其只读副本c.Copy()
启动多个服务
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)
}
}
HTML渲染
对于HTML渲染等不实用的不做介绍
使用静态文件
r.Static("/static", "./static")
Cookies
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie") // 获取Cookie
if err != nil {
cookie = "NotSet"
// 设置Cookie
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}
websocket
原生http+ws
package main
import (
"flag"
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var addr = flag.String("addr", "localhost:8080", "http service address")
var upgrader = websocket.Upgrader{
// 解决跨域问题
CheckOrigin: func(r *http.Request) bool {
return true
},
} // use default options
func ws(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
err = c.WriteMessage(mt, message)
if err != nil {
log.Println("write:", err)
break
}
}
}
func main() {
flag.Parse()
log.SetFlags(0)
http.HandleFunc("/ws", ws)
fmt.Println(*addr)
log.Fatal(http.ListenAndServe(*addr, nil))
}
使用gin
package control
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
// 解决跨域问题
CheckOrigin: func(r *http.Request) bool {
return true
},
} // use default options
func cmdWebSocket(c *gin.Context) {
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer ws.Close()
for {
mt, message, err := ws.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
err = ws.WriteMessage(mt, message)
if err != nil {
log.Println("write:", err)
break
}
}
}