Web is Easy,Let's try Gin:
一. 快速启动
下载gin的包:
go get github.com/gin-gonic/gin
编写gin的hello world如下:
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.GET("/hello", func(context *gin.Context) { // 注册一个路径的处理函数
context.Writer.WriteString("你好!")
})
router.Run(":80") // 启动端口设置为80
}
访问一下:
go的原生http启动方法是通过net/http
包来启动的,通过http.ListenAndServer(addr, handler)
来启动服务,在这个包种有一个结构体叫Server
,http服务要通过它的各种方法来做,http.ListenAndServe
也是调用了server.ListenAndServe
,上面的router
其实是一个引擎,也可以说是一个处理器,Run
方法实际也调用了http.ListenAndServe()
这个方法,不过第二个参数传的是router本身,用原生的方法的话不传就是一个默认的defaultServeMux
,http.handleFunc
其实就是在这个mux
中注册一些东西。
二. 不同种类的Http请求方法
gin支持Get
Post
Put
Delete
一系列的Restful风格的请求方式,使用的方式如下:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(context *gin.Context) {
context.Writer.WriteString("GET方法")
})
router.POST("/", func(context *gin.Context) {
context.Writer.WriteString("POST方法")
})
router.PUT("/", func(context *gin.Context) {
context.Writer.WriteString("PUT方法")
})
router.DELETE("/", func(context *gin.Context) {
context.Writer.WriteString("DELETE方法")
})
router.Run(":80")
}
三. 获取不同位置的参数
3.1 获取query参数
query参数即为url
中问号后面的键值对:
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.GET("/", func(context *gin.Context) {
// 如果没有这个参数想给个默认值的话可以用DefaultQuery("name", "默认值")
name := context.Query("name") // 获取参数,URL:http://localhost?name=jerry
context.Writer.WriteString("hello " + name) // 输出hello jerry
})
router.Run(":80")
}
3.2 获取Post的表单参数
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.POST("/", func(context *gin.Context) {
name := context.PostForm("name")
context.Writer.WriteString("hello " + name)
})
router.Run(":80")
}
3.3 获取path路径参数
是url
路径上的一部分,比如路径可以写成这样:http://localhost/jerry
jerry就是请求的参数,获取方法如下:
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.GET("/:name", func(context *gin.Context) { // 这种属于精准匹配,用*是模糊匹配,精准匹配此处必须有值,模糊匹配这个可以没有,后面也随便填
name := context.Param("name")
context.Writer.WriteString("hello " + name)
})
router.Run(":80")
}
3.4 获取json数据
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.POST("/:name", func(context *gin.Context) {
nameBytes, _ := context.GetRawData() // 获取json的字符串,如果想用需要用json.Unmarshal(nameBytes, &obj)解析出来
context.Writer.WriteString("hello " + string(nameBytes))
})
router.Run(":80")
}
3.5 参数绑定
上面的调用方法在实际使用过程中其实是比较麻烦的,尤其是json的绑定方法,可以使用context.ShouldBind(&obj)
方法自动绑定到一个对象上去使用,也有对应的context.ShouldBindQuery()
和context.ShouldBindJSON()
方法可以使用
四. 路由
4.1 普通的路由
就像上面的写法即可
如果想要让所有的请求方法(get、post、delete等)在该路径下都处理的话可以用any方法:
func main() {
router := gin.Default()
router.Any("/hello", func(context *gin.Context) { // 这样所有的hello请求不管是什么方式都会匹配
context.Writer.WriteString("hello")
})
router.Run(":80")
}
404处理的方法可以用NoRoute
解决,参数就是一个func(context *gin.Context)
即可
4.2 路由分组
可以将一个拥有共同前缀的路由划分成一个路由组,如下:
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
group := router.Group("/groups")
{
group.GET("/hello", func(context *gin.Context) {
context.Writer.WriteString("hi")
})
}
router.Run(":80")
}
访问http://localhost/groups/hello
即可,group.GET
默认已经填上了groups
前缀,group
也同样拥有Get
Post
等请求方法,并且可以嵌套使用。这里router
是没有默认前缀的
五. 中间件middleware
需要实现一个HandlerFunc
类型的函数,就是上面处理函数的样子,然后使用router.Use(func)
即可实现
func timeDurationMiddleware(context *gin.Context) {
now := time.Now().Unix()
context.Next() // tag
timeCost := time.Now().Unix() - now
context.Writer.WriteString(strconv.Itoa(int(timeCost)))
}
func main() {
router := gin.Default()
router.Use(timeDurationMiddleware)
group := router.Group("/groups")
{
group.GET("/hello", func(context *gin.Context) {
time.Sleep(time.Second + 1)
context.Writer.WriteString("hi ")
})
}
router.Run(":80")
}
这样就实现了一个记录调用时间的中间件,这里面的Next()
函数表示先执行后面的handlerFunc
,按照定义的顺序在访问http://localhost/groups/hello
时会先走全局注册的timeDurationMiddleware
中间件,然后走hello
请求的那个处理函数,那么调用了context.Next
就会在那个位置tag注释的那个位置停下,去执行下面处理函数,走完之后再回去执行tag下面的代码,类似于一个栈的过程。还有一个函数是context.Abort()
,表示当前这个函数执行完就不走下面的中间件和处理函数了。
gin的Default默认了两个中间件,可以使用gin.New()来获取一个没有中间件的引擎handler
六. 自定义Log
较为麻烦,还没整理清原理,后续补充~
七. 参数校验
gin自带了validation的验证,使用的时候需要加tag并且用ShouldBind
的方式:
type People struct {
Name string `json:"name" binding:"required"`
}
func main() {
router := gin.Default()
group := router.Group("/groups")
{
group.POST("/hello", func(context *gin.Context) {
p := new(People)
if err := context.ShouldBindJSON(p); err != nil {
fmt.Println("出错了", err.Error())
} else {
fmt.Println(p)
}
})
}
router.Run(":80")
}
这是一个例子,要求name字段必填,如果不填就会出现第一行的错误。
还有一些常见的验证如下(多种验证用逗号分隔,不要乱加空格):
eqfield= // 需要和该struct的哪个属性相同
oneof=a b c // 需要是abc其中的一个
email // 需要符合邮箱格式
gt=3 // 大于3
gte=3 // 大于等于3
eq=3 // 等于3
len=5 // slice或者string的长度等于5
八. 权限中间件
可以使用第三方库jwt-go
来实现jwt的认证,jwt的三部分分别是头部,负载,签名,代码如下:
package jwt
import (
"github.com/dgrijalva/jwt-go"
"time"
)
const (
TokenExpireDuration = time.Hour * 2 // token的过期时间
)
var jwtSecret = []byte("密钥")
type MyClaims struct {
UserID int64 `json:"user_id"` // 自定义的一些信息,属于负载的内容
Username string `json:"username"`
jwt.StandardClaims // 这里有一些认证机构,过期时间等常见的负载信息
}
func GenToken(userId int64, username string) (string, error) {
c := MyClaims{ // 构造一个负载
UserID: userId,
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
Issuer: "bluebell",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 用HS256签名
return token.SignedString(jwtSecret) // jwtSecret是盐值,用HS256盐值加密
}
func ParseToken(tokenString string) (*MyClaims, error) { // 解析一个token,这里过期等也会返回一个错误
var claims = new(MyClaims)
_, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
return claims, nil
}
然后实现一个认证的中间件:
// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
// 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
// 这里假设Token放在Header的Authorization中,并使用Bearer开头
// 这里的具体实现方式要依据你的实际业务情况决定
authHeader := c.Request.Header.Get("Authorization")
// 获取token之后做一些合法的校验
// valid code ...
mc, err := jwt.ParseToken(parts[1])
if err != nil {
response.ResponseError(c, response.CodeInvalidToken)
c.Abort() // 如果不合法要中断这次请求
return
}
c.Next() // 后续的处理函数可以用过c.Get("userID")来获取当前请求的用户信息
}
}
九. Https支持
首先需要有证书,然后将ListenAndServe
修改为ListenAndServeTLS()
即可(或者调用gin引擎的RunTLS):
func main() {
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
group := router.Group("/groups")
{
group.POST("/hello", func(context *gin.Context) {
p := new(People)
if err := context.ShouldBindJSON(p); err != nil {
fmt.Println("出错了", err.Error())
} else {
fmt.Println(p)
}
})
}
router.RunTLS(":443", "server.pem", "server.key")
}
十. 优雅关闭
可以通过第三方的endless
实现,但是可以手写一个,不用引入第三方包:
// +build go1.8
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,
}
// ListenAndServe会阻塞,所以要异步启动
// 这里没有用http.ListenAndServe和router.Run启动,因为这样我们拿不到server对象,无法优雅的shutdown
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 一个信号通道,大小一定要开1,不然限免无法阻塞了
quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
// 将两种停止方式注册到quit通道中
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 阻塞
<-quit
log.Println("Shutting down server...")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// shutdown中如果5s还没结束,那么会有个select接收到上面的超时错误并且返回回来
// 如果正常优雅的结束掉了,那么会调用cancel函数发出一个正常的结束
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}
十一. 一个进程启动多个服务
启动两个协程分别去启动server即可,大概的写法如下:
func main() {
r1 := gin.New()
r2 := gin.New()
var g errgroup.Group // 因为防止阻塞,所以要有一个协程启动,防止主线程G了,用它阻塞一下
g.Go(func() error {
return r1.Run(":8080") // server.ListenAndServe也可,本质都一样
})
g.Go(func() error {
return r2.Run(":8081")
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}