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() // 监听并在 0.0.0.0:8080 上启动服务
}
首先进行初始化 gin.Default(), 接着定义了一个叫做 /ping 的路由, 最后直接启动了 r.Run()。简单可以总结为下面几个过程:
首先, 查看下 gin.Default() 的过程:
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
Default 的主要功能是初始化 Engine, 然后加载了两个中间件, 用于日志记录和恢复。Engine 的初始化有两种方式, Default() 和 New(),实际上Default()在内部也调用了New()方法。
再看一下, 添加中间件的过程。
// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
添加中间件, 实际上是在 RouterGroup 上做注册,其内部包含路由路径和中间件数组handlers。所以添加中间件只是在 Handlers 中新加一个元素。
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
func (group *RouterGroup) returnObj() IRoutes {
if group.root {
return group.engine
}
return group
}
web 服务器最主要的当然是定义路由和处理函数了。
Engine 的内部使用了 RouterGroup, 所以其实上各种 HTTP 方法都是注册在 RouterGroup 上的. 具体看一下 GET 方法:
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers)
}
通过注释和代码, 我们可以知道, GET 只是一个快捷方式, 其实所有的 HTTP 方法注册都是由 router.Handle 处理的:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
handle 的核心语句是 group.engine.addRoute(httpMethod, absolutePath, handlers)
。这一点我们之后再详细解释,总之, 到这里, 路由已经注册好了。
r.Run()源码如下:
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() {
debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
这是一个阻塞的方法。内部使用 net/http 包的 ListenAndServe 函数:
func ListenAndServe(addr string, handler Handler) error
第二个参数的类型是 Handler, 一猜就知道应该是接口类型, 看一下具体要实现什么:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
engine.handleHTTPRequest©, 这用于处理 HTTP 请求:主要根据 HTTP 方法和路径从 engine.trees 找到 对应的 handlers。handlers的运行借助于Next(),可以将 c.Next() 理解为控制流转移, 每当运行 c.Next(), 实际上是运行下一个 handler. 有点类似递归时的调用栈。
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
我们都知道开发一个HTTP服务,首先需要启动一个TCP监听,然后需要有一些列的handler来处理具体的业务逻辑,最后在再将具体的业务逻辑通过HTTP协议约定和相关的Method和URL进行绑定,以此来对外提供具体功能的HTTP服务。
在Gin框架与之对应的就是如下几个模型:
Engine结构体不全部展示了,这里挑几个具备代表性的属性做说明:
type Engine struct {
...
// 路由组,在实际开发过程中我们通常会使用路由组来组织和管理一些列的路由. 比如: /apis/,/v1/等分组路由
RouterGroup
// methodTrees是methodTree的切片(methodTree是一个包含请求方法和node指针的结构体,node是一个管理path的节点树)
trees methodTrees
...
}
初始化Engine的方式:
1.1 RouterGroup
Gin 的 Engine 结构体内嵌了 RouterGroup 结构体,定义了 GET,POST 等路由注册方法。
1.2 methodTrees
Engine 中的 trees 字段定义了路由逻辑。trees 是 methodTrees 类型(其实就是 []methodTree数组,初始化语句是trees: make(methodTrees, 0, 9))
,不同请求方法的路由在不同的树(methodTree)中。接下来详细说明。
Gin添加路由主要是由 addRoute 完成:
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
//三个断言,判断path格式、method不为空和处理器handler数目至少为1
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
//根据method找到对应的methodTree
root := engine.trees.get(method)
//如果methodTree不存在,那么就创建一个methodTree
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{
method: method, root: root})
}
//添加路由
root.addRoute(path, handlers)
}
而methodTree的结构如下:
type methodTree struct {
method string
root *node
}
type methodTrees []methodTree
前面添加路由的代码中第一步是找到 root, 即 root := engine.trees.get(method), 结合 get 代码, 我们可以发现 methodTrees 实际上根据 HTTP 方法分类的, 每种方法都对应一颗树:
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
下一步if root == nil ,如果get出来的root不存在,那么就就新建一棵树 methodTree。
再看一下树的节点是如何定义的:
type nodeType uint8
const (
static nodeType = iota // default
root
param
catchAll
)
type node struct {
path string
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
fullPath string
}
数据结构已经了解了, 看一下路由到底是如何添加的, 即 root.addRoute(path, handlers)
。
// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
numParams := countParams(path)
parentFullPathIndex := 0
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
...代码太长了,省略
} else {
// Empty tree
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
}
}
代码有些长,一步一步分析:
先根据 if 语句分为两种情况if len(n.path) > 0 || len(n.children) > 0
:
当树是非空的,使用算法的就是Radix 树, 是Trie树的紧凑版变种,其中作为唯一子节点的每个节点都与其父节点合并。比如, 当前节点 node.path = “/ping”, 遇到 path = “/pong” 时就会分裂, 公共前缀的长度 i=2, 因此节点会分裂为 node.path = “/p” 和 node.path = “ing”. 分裂出来的后一个节点会占据当前节点的大部分属性.
在构建Radix路径的时候,会做节点的“公共前缀”分裂,然后 对新加入的节点做handlers的绑定。
// insert remaining path part and handle to the leaf
n.path = path[offset:]
n.handlers = handlers
n.fullPath = fullPath
使用Engine结构体中提供的相关方法,我们就可以快速的启动一个HTTP服务了,但是如何对外暴露一个URL来简单实现一个HTTP的数据传输呢,这个时候就需要使用Router中的方法了。
Gin框架中Router相关的结构体:
// RouterGroup 结构体
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
// IRoutes 接口
type IRoutes interface {
Use(...HandlerFunc) IRoutes
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
// IRouter接口
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
在Gin框架中由Router结构体来负责路由和方法(URL和HTTP方法)的绑定,Router内的Handler采用Context结构体来处理具体的HTTP数据传输方式,比如HTTP头部,请求体参数,状态码以及响应体和其他的一些常见HTTP行为。
type Context struct {
// 一个包含size,status和ResponseWriter的结构体
writermem responseWriter
// http的请求体(指向原生的http.Request指针)
Request *http.Request
// ResonseWriter接口
Writer ResponseWriter
// 请求参数[]{"Key":"Value"}
Params Params
handlers HandlersChain
index int8
// http请求的全路径地址
fullPath string
// gin框架的Engine结构体指针
engine *Engine
// 每个请求的context中的唯一键值对
Keys map[string]interface{
}
// 绑定到所有使用该context的handler/middlewares的错误列表
Errors errorMsgs
// 定义了允许的格式被用于内容协商(content)
Accepted []string
// queryCache 使用url.ParseQuery来缓存参数查询结果(c.Request.URL.Query())
queryCache url.Values
// formCache 使用url.ParseQuery来缓存PostForm包含的表单数据(来自POST,PATCH,PUT请求体参数)
formCache url.Values
}
Context 是在每次接受请求的时候初始化的:
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
里面用到了 sync.Pool, sync.Pool 适用于缓存已分配但未使用的 items, 以便后续重用, 并减轻垃圾回收的压力。
接着看一下如何获取请求参数, 比如 URL 中的参数, GET 中的 query, 或者是 POST 中的 data。
func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
func (c *Context) Query(key string) string {
value, _ := c.GetQuery(key)
return value
}
... 只展示几个
模型绑定是一个非常有用的能力, 尤其是和验证结合在一起. 处理请求参数时, 一大重点就是验证.
Gin 支持两种类型的绑定, Must bind 和 Should bind. 请求类型则支持 JSON, XML, YAML 和标准表单绑定.
// Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding
// "application/xml" --> XML binding
// otherwise --> returns an error.
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj interface{
}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.MustBindWith(obj, b)
}
// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj interface{
}) error {
return c.MustBindWith(obj, binding.JSON)
}
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
func (c *Context) BindXML(obj interface{
}) error {
return c.MustBindWith(obj, binding.XML)
}
// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
func (c *Context) BindYAML(obj interface{
}) error {
return c.MustBindWith(obj, binding.YAML)
}
....只展示几个
从上面的代码可以发现, MustBindWith 其实是 ShouldBindWith 的包装, 具体内容还是要看 ShouldBindWith。但实际上 ShouldBindWith 也只是调用了 binding.Binding 上的方法而言:
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{
}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
Bingding :
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
Name() string
Bind(*http.Request, interface{
}) error
}
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, interface{
}) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{
}) error
}
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
JSON = jsonBinding{
}
XML = xmlBinding{
}
Form = formBinding{
}
Query = queryBinding{
}
FormPost = formPostBinding{
}
FormMultipart = formMultipartBinding{
}
ProtoBuf = protobufBinding{
}
MsgPack = msgpackBinding{
}
YAML = yamlBinding{
}
Uri = uriBinding{
}
Header = headerBinding{
}
)
以 JSON 为例,jsonBinding实际上是实现了json的编码和解码而已。
解码的最后一步是验证, 调用了 validate 函数:
func validate(obj interface{
}) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}
看完了请求参数的获取和模型绑定之后, 来看看响应是如何发送的。先来看一下 Context 中用到的 responseWriter 类型和 ResponseWriter 类型:
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
...
}
ResponseWriter 接口组合了 http 包中用于响应的数据结构, 所有的方法上都有注释. 而 responseWriter 实际上就是实现了 ResponseWriter 接口的结构体。
Writer 是用于写入响应的, 而从 writermem 名字的后缀, 可以推断出这和内存有关。
内容协商通过 Accept Header 实现, 用于为不同类型的客户端提供不同类型的资源, 比如协商网页语言或响应格式等。在此不做赘述了。