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本身,用原生的方法的话不传就是一个默认的defaultServeMuxhttp.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)
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值