Go Web开发(Gin框架)简易入门教程

2,194 阅读8分钟

net/http标准库

在讲述框架之前,先来说说Go语言的内置net/http包,其实net/http已经为我们提供了基础的路由函数组合和丰富的功能函数,如果你只是需要简单的API编写,net/http就完全足够了。 下面就来编写一个最简单的web服务程序:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	// 该方法接收一个路由匹配的字符串,以及一个 func(ResponseWriter, *Request) 类型的函数
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8000", nil)) // 监听本地8000端口
}

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) // r.URL.Path 输出url的路径
}

浏览器访问结果:

image.png

来进阶一下,看看如何解析常见的请求参数类型,以及如何返回json格式。

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

func main() {
	// 该方法接收一个路由匹配的字符串,以及一个 func(ResponseWriter, *Request) 类型的函数
	http.HandleFunc("/", handler)
	http.HandleFunc("/get", handleGet)
	http.HandleFunc("/postJson", handlePostJson)
	http.HandleFunc("/postForm", handlePostForm)
	http.HandleFunc("/responseJson", handleResponseJson)
	log.Fatal(http.ListenAndServe(":8000", nil)) // 监听本地8000端口
}

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) // r.URL.Path 输出url的路径
}

// 处理GET请求查询参数
func handleGet(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query()
	id := query.Get("id")
	fmt.Fprintf(w, "GET: id=%s\n", id)
}

// 处理application/json类型的POST请求
func handlePostJson(w http.ResponseWriter, r *http.Request) {
	// 根据请求body创建一个json解析器实例
	decoder := json.NewDecoder(r.Body)
	// 用于存放参数key=value数据
	var params map[string]string
	// 解析参数 存入map
	decoder.Decode(&params)
	fmt.Fprintf(w, "POST json: username=%s, password=%s\n", params["username"], params["password"])
}

// 处理application/x-www-form-urlencoded类型的POST请求
func handlePostForm(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	username := r.Form.Get("username")
	password := r.Form.Get("password")
	fmt.Fprintf(w, "POST form-urlencoded: username=%s, password=%s\n", username, password)
}

// 返回JSON数据格式
func handleResponseJson(w http.ResponseWriter, r *http.Request) {
	type Response struct {
		Code int         `json:"code"`
		Msg  string      `json:"msg"`
		Data interface{} `json:"data"`
	}
	res := Response{
		200,
		"success",
		"admin",
	}
	json.NewEncoder(w).Encode(res) // 关键
}

/get请求结果:

image.png

/postJson请求结果:

image.png

/postForm请求结果:

image.png

/responseJson请求结果:

image.png

到这里,基本的使用就已经介绍的差不多了,关于更多net/http标准库的内容可以直接查看标准库文档:studygolang.com/pkgdoc

gin框架

引入官方的介绍: Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架,由于 httprouter,速度提高了近 40 倍。如果你需要极好的性能,使用 Gin 吧。

特性:

  • 快速:路由不使用反射,基于Radix树,内存占用少。
  • 中间件:HTTP请求,可先经过一系列中间件处理,例如:Logger,Authorization,GZIP等。这个特性和 NodeJs 的 Koa 框架很像。中间件机制也极大地提高了框架的可扩展性。
  • 异常处理:服务始终可用,不会宕机。Gin 可以捕获 panic,并恢复。而且有极为便利的机制处理HTTP请求过程中发生的错误。
  • JSON:Gin可以解析并验证请求的JSON。这个特性对Restful API的开发尤其有用。
  • 路由分组:例如将需要授权和不需要授权的API分组,不同版本的API分组。而且分组可嵌套,且性能不受影响。
  • 渲染内置:原生支持JSON,XML和HTML的渲染。

1.快速开始

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // 默认监听本地8080端口,如果需要更改可以使用 r.Run(":9000")
}

浏览器访问:

image.png

控制台输出:image.png

在这段代码中gin做了什么?

  1. 首先,我们使用了gin.Default()生成了一个实例,赋值给r
  2. 接下来,我们使用r.Get("/ping", ...)声明了一个路由,告诉 Gin 什么样的URL 能触发传入的函数,这个函数返回我们想要显示在用户浏览器中的信息。
  3. 最后用 r.Run()函数来让应用运行在本地服务器上,默认监听端口是 8080,可以传入参数设置端口,例如r.Run(":9999")即运行在9999端口。

2.路由

路由方法有 GET, POST, PUT, PATCH, DELETEOPTIONS,还有Any,可匹配以上任意类型的请求。

r.GET("/someGet", func)
r.POST("/somePost", func)
r.PUT("/somePut", func)
r.DELETE("/someDelete", func)
r.PATCH("/somePatch", func)
r.HEAD("/someHead", func)
r.OPTIONS("/someOptions", func)
//处理所有的请求方法
r.Any("/any", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "请求类型": c.Request.Method,
    })
})

路由参数

// 无参数
r.GET("/", func (c *gin.Context)  {
	c.String(http.StatusOK, "Hello")
})
// 路径参数,匹配 /path/admin
r.GET("/path/:name", func(c *gin.Context) {
	name := c.Param("name")//取得URL路径中参数name的值
	c.String(http.StatusOK, "Hello %s", name)
})
// 星号路由参数,匹配所有,不建议使用,如/all/*id
r.GET("/all/*id", func(c *gin.Context) {
	id := c.Param("id")
	c.String(http.StatusOK, "id is  %s", id)
})
// 查询参数,匹配 user?name=xxx&role=xxx,role可选
r.GET("/user", func(c *gin.Context) {
	name := c.Query("name")//查询请求URL后面的参数name的值
	role := c.DefaultQuery("role", "teacher")//如果获取不到,会赋值默认值"teacher"
	c.String(http.StatusOK, "%s is a %s", name, role)
})
// form表单
r.POST("/form", func(c *gin.Context) {
	username := c.PostForm("username")
	password := c.DefaultPostForm("password", "000000") // 可设置默认值
	c.JSON(http.StatusOK, gin.H{
		"username": username,
		"password": password,
	})
})
// json参数
r.POST("/json", func(c *gin.Context) {
	type Body struct {
		Email    string `json:"email"`
		Username string `json:"username"`
	}
	var body Body
	c.ShouldBind(&body)//绑定参数,将参数解析到body结构体中
	c.JSON(http.StatusOK, body)
})
// 数组参数,匹配多选业务如 array?answer=xxx&answer=xxx&answer=xxx,key一样,value不同
r.GET("/array", func(c *gin.Context) {
    array := c.QueryArray("answer")
	c.JSON(http.StatusOK, array)
})
// map参数,字典参数,匹配 map?ids[a]=123&ids[b]=456&ids[c]=789
r.GET("/map", func(c *gin.Context) {
	c.JSON(http.StatusOK, c.QueryMap("ids"))
})

路由分组

v1Group := r.Group("/v1")
{
    v1Group.GET("/user", func(c *gin.Context) {
        c.String(200, "这是v1版本/v1/user")
	})
}

v2Group := r.Group("/v2")
{
	v2Group.GET("/user", func(c *gin.Context) {
		c.String(200, "这是v2版本/v2/user")
	})
}

3.输出渲染格式

gin可以很方便的渲染输出数据的格式

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/someString", func(c *gin.Context) {
		c.String(http.StatusOK, "string")
	})

	// gin.H 是 map[string]interface{} 的一种快捷方式
	r.GET("/someJSON", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/moreJSON", func(c *gin.Context) {
		// 你也可以使用一个结构体
		var msg struct {
			Name    string `json:"user"`
			Message string
			Number  int
		}
		msg.Name = "Lena"
		msg.Message = "hey"
		msg.Number = 123
		// 注意由于`json:"user"`的关系 msg.Name 在 JSON 中变成了 "user"
		// 将输出:{"user": "Lena", "Message": "hey", "Number": 123}
		c.JSON(http.StatusOK, msg)
	})

	r.GET("/someXML", func(c *gin.Context) {
		c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/someYAML", func(c *gin.Context) {
		c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.Run()
}

4.中间件

回到快速开始中,我们使用gin.Default()生成了一个实例,查看gin.Default()的源码,可以发现

image.png

Default函数会默认绑定两个已经准备好的中间件,Logger Recovery,分别帮助我们打印日志输出和painc处理。 从中可以看到,Gin的中间件是通过Use方法设置的,它接收一个可变参数,所以我们同时可以设置多个中间件。

image.png

而一个Gin的中间件,其实就是Gin定义的一个HandlerFunc

image.png

它在我们Gin中经常使用,比如:

// 其中func(c *gin.Context){} 就是一个HandlerFunc类型函数
r.GET("/", func(c *gin.Context) {
    c.String(200, "hello")
})

我们现在来尝试如何使用中间件:

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	// 创建一个不包含中间件的路由器实例
	r := gin.New()

	// 全局使用中间件
	// 使用gin自带的 Logger 日志中间件
	r.Use(gin.Logger())
	// 使用gin自带的 Recovery 中间件从任何 panic 恢复,如果出现 panic,它会写一个 500 错误。
	r.Use(gin.Recovery())
    
    // 以上代码相当于 r := gin.Default()

	// 添加自定义的全局中间件
	r.Use(middleware.Cors())

	// 单个路由添加中间件,可以添加任意多个
	r.GET("/path", middleware.JWT())

	// 路由组中添加中间件,中间件只在该路由组中产生作用
	// user := r.Group("/user", middleware.AuthRequired())
	user := r.Group("/user")
	user.Use(middleware.AuthRequired())
	{
		user.POST("/login", Login)
	}
	r.Run()
}

学会使用中间件后,如何自定义一个自己的中间件呢?很简单,比如想要自定义一个计算请求时间的requestTime中间件:

package main

import (
	"fmt"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.Use(requestTime())
	r.GET("/", func(c *gin.Context) {
		fmt.Println("我到这里了 hello")
		c.String(200, "hello")
	})
	r.Run()
}

func requestTime() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now() // 记录开始时间
		fmt.Println("这里被我拦截住了")
		fmt.Println("因为调用了c.Next() 我现在要走了")
		c.Next() // 立即调用下一个HandlerFunc(会产生调用耗时)
		fmt.Println("我又回来了")
		fmt.Println(time.Since(start)) // 打印本次请求处理时间差
	}
}

image.png

可以看到c.Next()在这里发挥了很大的作用,其中在c.Next()之前的操作我们一般用来做验证处理,访问是否允许之类的。 之后的操作一般就是用来做总结处理,比如格式化输出、响应结束时间,响应时长计算之类的。

在gin中间件中,除了c.Next(),相对应的还有c.Abort(),而c.Abort()的作用则是阻止后续的处理函数,比如检验到是非法请求时,阻断接下来的操作。

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.Use(authority())
	r.GET("/path/:name", func(c *gin.Context) {
		fmt.Println("欢迎")
		c.String(http.StatusOK, "Hello %s", c.Param("name"))
	})
	r.Run()
}

func authority() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("开始-权限控制")
		isAdmin := c.Param("name") == "admin"
		if isAdmin {
			c.Next()
		} else {
			c.Abort() //不会再执行接下去的HandlerFunc了
			c.String(http.StatusOK, "对不起,您不是管理员")
		}
		fmt.Println("结束-权限控制")
	}
}

情况①:

image.png

image.png

情况②:

image.png

image.png

程序在调用了c.Abort()之后,就只会继续往下执行中间件的代码,不会再跳到我们定义的接口HandlerFunc去了,其实就是起到了拦截的作用。

总结一下c.Next()作用是立即执行下一个HandlerFunc,完后会跳转回来继续执行c.Next()接下去的代码, 而c.Abort()则是阻断执行下一个HandlerFunc,仅会执行接下去的代码,常用于权限控制拦截操作。

有时候我们还需要跨中间件取值,可以使用c.Set(key, value)设置值,使用c.Get(key)取值

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.Use(authority())
	r.GET("/", func(c *gin.Context) {
		value, ok := c.Get("key")
		if ok {
			fmt.Println(value)
		}
		c.String(http.StatusOK, "Hello")
	})
	r.Run()
}

func authority() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set("key", "你好")
		c.Next()
	}
}

请求结果:

image.png

基本介绍到这里,关于gin的文档可以参考:learnku.com/docs/gin-go…