前言: Gin的词源是金酒, 又称琴酒, 是来自荷兰的一种烈性酒。node
在Go中,有一个常常说起的web框架,就是gin web,具有高性能,可灵活定制化的特色,既然它是如此被看好,在深刻了解它以前,不妨先看下他是基于什么实现的。git
根据Git做者描述,Gin的高性能得益于一个叫httprouter的框架生成的,顺着源头看,咱们先针对httprouter, 从HTTP路由开始,做为Gin框架的引入篇。github
在扎堆深刻以前, 先梳理一下路由的概念:
路由: 大概意思是经过转发数据包实现互联,好比生活中常见的物理路由器,是指内网与外网之间信息流的分配。
同理,软件层面也有路由的概念,通常暴露在业务层之上,用于转发请求到合适的逻辑处理器。golang
The router matches incoming requests by the request method and the path.web
程序的应用上,常见的如对外服务器把外部请求打到Nginx网关,再路由(转发)到内部服务或者内部服务的“控制层”,如Java的springMVC,Go的原生router等对不一样请求转发到不一样业务层。
或者再具体化点说,好比不一样参数调用同名方法,如Java的重载,也能够理解为程序根据参数的不一样路由到相应不一样的方法。spring
Git的README文档上,httprouter开门见山的展现了它的一个常见功能,
启动一个HTTP服务器,而且监听8080端口,对请求执行参数解析,仅仅几行代码,当我第一次见到这种实现时候,确实以为go这种实现至关优雅。shell
router.GET("/", Index)
//传入参数name
router.GET("/hello/:name", Hello)
func Hello(w http.ResponseWriter, r *http.Request) {
//经过http.Request结构的上下文能够拿到请求url所带的参数
params := httprouter.ParamsFromContext(r.Context())
fmt.Fprintf(w, "hello, %s!\n", params.ByName("name"))
}
//启动监听
http.ListenAndServe(":8080", router)
复制代码
在观察了如何创建一个监听程序以后,挖掘这种优雅是如何封装实现以前,咱们要先了解,在原生Go中,每一个Router
路由结构都实现了http.Handler
接口,Handler
只有一个方法体,就是ServerHTTP
,它只有一个功能,就是处理请求,作出响应。bash
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
复制代码
题外话,Go中比较倾向于KISS或者单一职责,把每一个接口的功能都单一化,有须要再进行组合,用组合代替继承,后续会把它看成一个编码规范来看。服务器
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
复制代码
能够看到,在Go
原生库中,ServeHTTP()
实现体HandlerFunc
就是个func函数类型,具体实现又是直接套用HandlerFunc
进行处理,我没有在套娃哈,是否是有种“我实现我本身”的赶脚。app
All in all, 咱们先抛开第三方库的封装,复习一下标准库,假如咱们想用原生http\server包搭建一个HTTP处理逻辑,咱们通常能够
方式1:
http.Server
做为其Handler
成员方式2:
ServeHTTP(w http.ResponseWriter, req *http.Request)
http.Server
做为其Handler
成员示例以下:
//方式1
func SelfHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "<h1>Hello!</h1>")
}
//方式2
type HelloHandler struct {
}
//HelloHandler实现ServeHTTP()接口
func (* HelloHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "<h1>Hello!</h1>")
}
s := &http.Server{
Addr: ":8080",
//方式1
Handler: SelfHandler,
//方式2
//Handler: &HelloHandler{},
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
复制代码
抛砖引玉:
以上就是Go标准库实现http服务的经常使用用法,如今进行拓展,假如咱们须要经过url去获取参数,如Get请求,localhost:8080/abc/1
Q: 咱们如何拿到abc或者1呢?
A: 其实有个相对粗暴的方法,就是硬解:
net/url
的Parse()
函数把8080
后面的那一段提取出来stings.split(ul, "/")
示例以下
func TestStartHelloWithHttp(t *testing.T) {
//fmt.Println(path.Base(ul))
ul := `https://localhost:8080/pixeldin/123`
parse, e := url.Parse(ul)
if e != nil {
log.Fatalf("%v", e)
}
//fmt.Println(parse.Path) // "/pixeldin/123"
name := GetParamFromUrl(parse.Path, 1)
id := GetParamFromUrl(parse.Path, 2)
fmt.Println("name: " + name + ", id: " + id)
}
//指定下标返回相对url的值
func GetParamFromUrl(base string, index int) (ps string) {
kv := strings.Split(base, "/")
assert(index < len(kv), errors.New("index out of range."))
return kv[index]
}
func assert(ok bool, err error) {
if !ok {
panic(err)
}
}
复制代码
输出:
name: pixeldin, id: 123
复制代码
这种办法给人感受至关暴力,并且须要记住每一个参数的位置和对应的值,而且多个url不能统一管理起来,每次寻址都是遍历。尽管Go标准库也提供了一些通用函数,好比下面这个栗子:
GET方式的url: https://localhost:8080/?key=hello
,
能够经过*http.Request
来获取,这种请求方式是在url中声明键值对,而后后台根据请求key进行提取。
//摘取自:https://golangcode.com/get-a-url-parameter-from-a-request/
func handler(w http.ResponseWriter, r *http.Request) {
keys, ok := r.URL.Query()["key"]
if !ok || len(keys[0]) < 1 {
log.Println("Url Param 'key' is missing")
return
}
// Query()["key"] will return an array of items,
// we only want the single item.
key := keys[0]
log.Println("Url Param 'key' is: " + string(key))
}
复制代码
可是,曾经沧海难为水。相信你们更喜欢开篇列举的那个例子,包括如今咱们习惯的几个主流的框架,都倾向于利用url的位置去寻参,固然httprouter
的优点确定不止在这里,这里只是做为一个了解httprouter
的切入点。
router.GET("/hello/:name", Hello)
router.GET("/hello/*name", HelloWorld)
复制代码
到这里先止住,后续咱们来追踪它们封装以后的底层实现以及是如何规划url参数的。
前面提到,全部路由结构都实现了http.Handler
接口的ServeHTTP()
方法,咱们来看下httprouter
基于它的实现方式。
在httprouter中,ServeHTTP() 的实现结构就叫*Router
,它内部封装了用于检索url的tree结构,几个经常使用的布尔选项,还有几个也是基于http.Handler
实现的默认处理器,它的实现以下:
// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.PanicHandler != nil {
defer r.recv(w, req)
}
path := req.URL.Path
if root := r.trees[req.Method]; root != nil {
//getValue()返回处理方法与参数列表
if handle, ps, tsr := root.getValue(path); handle != nil {
//匹配执行
handle(w, req, ps)
return
} else if req.Method != http.MethodConnect && path != "/" {
//...
}
}
if req.Method == http.MethodOptions && r.HandleOPTIONS {
// Handle OPTIONS requests
//...
} else if r.HandleMethodNotAllowed { // Handle 405
//执行默认处理器...
}
// Handle 404
if r.NotFound != nil {
r.NotFound.ServeHTTP(w, req)
} else {
http.NotFound(w, req)
}
}
复制代码
到这里能够大体猜想它把处理method注入到内部trees结构,利用传入url在trees进行匹配查找,对执行链进行相应执行。 能够猜想这个Router.trees
包含了handle和相应的参数,接着咱们进入它的路由索引功能,来看下它是怎么实现++url匹配++与++参数解析++的。
结构梳理:
这个trees
存在tree.go源文件中,实际上是个map键值对,
key是HTTP methods(如GET/HEAD/POST/PUT等),method就是当前method与方法绑定上的节点
我在源码补充些注释,相信你们容易看懂。
// Handle registers a new request handle with the given path and method.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
// ...
func (r *Router) Handle(method, path string, handle Handle) {
if len(path) < 1 || path[0] != '/' {
panic("path must begin with '/' in path '" + path + "'")
}
//首次注册url,初始化trees
if r.trees == nil {
r.trees = make(map[string]*node)
}
//绑定http methods根节点,method能够是GET/POST/PUT等
root := r.trees[method]
if root == nil {
root = new(node)
r.trees[method] = root
r.globalAllowed = r.allowed("*", "")
}
//对http methods方法树的路径划分
root.addRoute(path, handle)
}
复制代码
Router.trees
map键值对的value是一个node
结构体,每一个HTTP METHOD 都是一个root节点,最主要的path分配是在这些节点的addRoute() 函数,
简单理解的话, 最终那些前缀一致的路径会被绑定到这个树的同一个分支方向上,直接提升了索引的效率。
下面我先列举出node
几个比较重要的成员:
type node struct {
path string
//标识 path是否后续有':', 用于参数判断
wildChild bool
/* 当前节点的类型,默认是0, (root/param/catchAll)分别标识(根/有参数/全路径)*/
nType nodeType
maxParams uint8
//当前节点优先级, 挂在上面的子节点越多,优先级越高
priority uint32
indices string
//知足前缀的子节点,能够延申
children []*node
//与当前节点绑定的处理逻辑块
handle Handle
}
复制代码
其中子节点越多,或者说绑定handle方法越多的根节点,priority优先级越高,做者有意识的对每次注册完成进行优先级排序。 引用做者的批注:
This helps in two ways:
- Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible.
- It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right.
优先级高的节点有利于handle的快速定位,相信比较好理解,现实中人流量密集处每每就是十字路口,相似交通枢纽。基于前缀树的匹配,让寻址从密集处开始,有助于提升效率。
由浅入深:
咱们先给路由器router注册几个做者提供的GET处理逻辑,而后开始调试,看下这个trees成员随着url新增有什么变化,
router.Handle("GET", "/user/ab/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
//do nothing, just add path+handler
})
router.Handle("GET", "/user/abc/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
//do nothing, just add path+handler
})
router.Handle(http.MethodGet, "/user/query/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
routed = true
want := httprouter.Params{httprouter.Param{"name", "gopher"}}
if !reflect.DeepEqual(ps, want) {
t.Fatalf("wrong wildcard values: want %v, got %v", want, ps)
}
})
复制代码
上述操做把【RESTful的GET方法,url路径,匿名函数handler】做为router.Handler()
的参数,router.Handler()
的操做上面咱们已经简单的分析过了,主要节点划分在其中的addRoute()
函数里面,下面咱们简单过一下它的逻辑
// 将当前url与处理逻辑放在当前节点
func (n *node) addRoute(path string, handle Handle) {
fullPath := path
n.priority++
//提取当前url参数个数
numParams := countParams(path)
// 若是当前节点已经存在注册链路
if len(n.path) > 0 || len(n.children) > 0 {
walk:
for {
// 更新最大参数个数
if numParams > n.maxParams {
n.maxParams = numParams
}
// 判断待注册url是否与已有url有重合,提取重合的最长下标
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
i := 0
max := min(len(path), len(n.path))
for i < max && path[i] == n.path[i] {
i++
}
/* 若是进来的url匹配长度大于于当前节点已有的url,则建立子节点 好比当前节点是/user/ab/ func1, 新进来一个/user/abc/ func2,则须要建立/user/ab的子节点/ 和 c/ 树状以下: |-/user/ab |--------|-/ func1 |--------|-c/ func2 以后若是再注册一个/user/a/ 与func3 则最终树会调整为: 优先级3 |-/user/a 优先级2 |--------|-b 优先级1 |----------|-/ func1 优先级1 |----------|-c/ func2 优先级1 |--------|-/ func3 */
if i < len(n.path) {
child := node{
path: n.path[i:],
wildChild: n.wildChild,
nType: static,
indices: n.indices,
children: n.children,
handle: n.handle,
priority: n.priority - 1,
}
// 遍历子节点,取最高优先级做为父节点优先级
for i := range child.children {
if child.children[i].maxParams > child.maxParams {
child.maxParams = child.children[i].maxParams
}
}
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handle = nil
n.wildChild = false
}
// Make new node a child of this node
if i < len(path) {
path = path[i:]
if n.wildChild {
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// Adding a child to a catchAll is not possible
n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
} else {
// Wildcard conflict
var pathSeg string
if n.nType == catchAll {
pathSeg = path
} else {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
}
c := path[0]
// slash after param
if n.nType == param && c == '/' && len(n.children) == 1 {
n = n.children[0]
n.priority++
continue walk
}
// Check if a child with the next path byte exists
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
//增长当前节点的优先级,而且作出位置调整
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// Otherwise insert it
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, fullPath, handle)
return
} else if i == len(path) { // Make node a (in-path) leaf
if n.handle != nil {
panic("a handle is already registered for path '" + fullPath + "'")
}
n.handle = handle
}
return
}
} else { // Empty tree
n.insertChild(numParams, path, fullPath, handle)
n.nType = root
}
}
复制代码
以上的大体思路是,把各个handle func()注册到一棵url前缀树上面,根据url前缀相同的匹配度进行分支,以提升路由效率。
参数查找:
接下来咱们看下httprouter是怎么将参数param封装在上下文里面的:
不难猜想分支划分的时候是经过判断关键字“:”来提取预接收的参数,这些参数是存字符串(字典)键值对,底层存储在一个Param
结构体:
type Param struct {
Key string
Value string
}
复制代码
关于上下文的概念在其余语言也挺常见,如Java Spring框架中的application-context
,用来贯穿程序生命周期,用于管理一些全局属性。
Go的上下文也在不一样框架有多种实现,这里咱们先初步了解Go程序最顶级的上下文是background()
,是全部子上下文的来源,相似于Linux系统的init()进程。
先举个栗子,简单列举在Go的context传参的用法:
func TestContext(t *testing.T) {
// 获取顶级上下文
ctx := context.Background()
// 在上下文写入string值, 注意须要返回新的value上下文
valueCtx := context.WithValue(ctx, "hello", "pixel")
value := valueCtx.Value("hello")
if value != nil {
/* 已知写入值是string,因此咱们也能够直接进行类型断言 好比: p, _ := ctx.Value(ParamsKey).(Params) 这个下划线实际上是go断言返回的bool值 */
fmt.Printf("Params type: %v, value: %v.\n", reflect.TypeOf(value), value)
}
}
复制代码
输出:
Params type: string, value: pixel.
复制代码
在httprouter中,封装在http.request
中的上下文实际上是个valueCtx,类型和咱们上面的栗子中valueCtx是同样的,框架中提供了一个从上下文获取Params键值对的方法,
func ParamsFromContext(ctx context.Context) Params {
p, _ := ctx.Value(ParamsKey).(Params)
return p
}
复制代码
利用返回的Params就能够根据key获取咱们的目标值了,
params.ByName(key)
复制代码
通过追寻,Params是来自一个叫getValue(path string) (handle Handle, p Params, tsr bool)
的函数,还记得上面列举的*Router路由实现的ServeHTTP()
接口吗?
//ServeHTTP() 函数的一部分
if root := r.trees[req.Method]; root != nil {
/** getValue(),返回处理方法与参数列表 **/
if handle, ps, tsr := root.getValue(path); handle != nil {
//匹配执行, 这里的handle就是上面的匿名函数func
handle(w, req, ps)
return
} else if req.Method != http.MethodConnect && path != "/" {
//...
}
}
复制代码
ServeHTTP()
其中有个getValue
函数,它的返回值有两个重要成员:当前路由的处理逻辑和url参数列表,因此在路由注册的时候咱们须要把params做为入参传进去。 像这样子:
router.Handle(http.MethodGet, "/user/query/:name",
//匿名函数func,这里的ps参数就是在ServeHttp()的时候帮你提取的
func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Println(params.ByName("name"))
})
复制代码
getValue()
在对当前节点n进行类型判断,若是是‘param’类型(在addRoute的时候已经根据url进行分类),则填充待返回参数Params。
//-----------go
//...github.com/julienschmidt/httprouter@v1.3.0/tree.go:367
switch n.nType {
case param:
// find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
//遇到节点参数
if p == nil {
// lazy allocation
p = make(Params, 0, n.maxParams)
}
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[1:]
p[i].Value = path[:end]
//...
复制代码
流程梳理:
So far,咱们再次概括一下httprouter
的路由过程:
type Handle func(http.ResponseWriter, *http.Request, Params)
的函数到该routerServeHTTP()
,用于提取当前url预期的参数而且供业务层使用以上这是这两天对httprouter的了解,顺便对go的Http有了进一步的认知,后续将尝试进入gin中,看下Gin是基于httprouter作了什么拓展以及熟悉常见用法。
julienschmidt/httprouter
github.com/julienschmi…