gin源码阅读(1)URL中的参数是怎样剖析的?

[复制链接]
发表于 2025-12-25 12:14:36 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
gin源码阅读(1)URL中的参数是怎样剖析的?

向gin服务器哀求时,URL哀求参数有两种情势:(1)在路径中的参数,比如"/user/2/send"中的"2"就是用户编号;(2)查询参数,比如“/user/list?page=1&pageSize=5”中的"page"和"pageSize"就是查询参数。
下面分别对这种URL参数举行解读,看看gin中是怎样剖析这两种参数的。
一、gin是怎样剖析URL中路径参数的?

先看一个官方给出的例子:
  1. func main() {
  2.     router := gin.Default()
  3.     // 此 handler 将匹配 /user/john 但不会匹配 /user/ 或者 /user
  4.     router.GET("/user/:name", func(c *gin.Context) {
  5.         name := c.Param("name")
  6.         c.String(http.StatusOK, "Hello %s", name)
  7.     })
  8.     // 此 handler 将匹配 /user/john/ 和 /user/john/send
  9.     // 如果没有其他路由匹配 /user/john,它将重定向到 /user/john/
  10.     router.GET("/user/:name/*action", func(c *gin.Context) {
  11.         name := c.Param("name")  // ref-1
  12.         action := c.Param("action")
  13.         message := name + " is " + action
  14.         c.String(http.StatusOK, message)
  15.     })
  16.     router.Run(":8080")
  17. }
复制代码
在ref-1处利用了Param(...)函数获取路径中"name"参数的值,我们详细看一下该函数的细节:
  1. // Param returns the value of the URL param.
  2. // It is a shortcut for c.Params.ByName(key)
  3. //
  4. //        router.GET("/user/:id", func(c *gin.Context) {
  5. //            // a GET request to /user/john
  6. //            id := c.Param("id") // id == "john"
  7. //            // a GET request to /user/john/
  8. //            id := c.Param("id") // id == "/john/"
  9. //        })
  10. func (c *Context) Param(key string) string {
  11.         return c.Params.ByName(key)
  12. }
复制代码
解释中写的很明白,Param(...)函数就是c.Params.ByName(key)的一个简写,我们继续看c.Params.ByName(key)细节:
  1. // ByName returns the value of the first Param which key matches the given name.
  2. // If no matching Param is found, an empty string is returned.
  3. func (ps Params) ByName(name string) (va string) {
  4.         va, _ = ps.Get(name)
  5.         return
  6. }
复制代码
ByName(...)函数就是在从Params中获取键值对数据,我们来看看Params结构体的界说:
  1. // Param is a single URL parameter, consisting of a key and a value.
  2. type Param struct {
  3.         Key   string
  4.         Value string
  5. }
  6. // Params is a Param-slice, as returned by the router.
  7. // The slice is ordered, the first URL parameter is also the first slice value.
  8. // It is therefore safe to read values by the index.
  9. type Params []Param
复制代码
现在有一个标题,这个Params是在哪儿赋值的呢?为了答复这个标题,我们先来看看gin是如那边置处罚http哀求的。
先看一下router.Run(":8080")函数的调用,如下所示:
  1. func (engine *Engine) Run(addr ...string) (err error) {
  2.         ...... // 省略其他代码
  3.         address := resolveAddress(addr)
  4.         debugPrint("Listening and serving HTTP on %s\n", address)
  5.         err = http.ListenAndServe(address, engine.Handler()) // ref-2
  6.         return
  7. }
复制代码
可以看到实在Run方法调用了go自带库中的http.ListenAndServe(...)方法,而且注册了处置处罚器engine.Handler(),这就很明白,http哀求就是被注册的handler处置处罚的,我们详细来看一下:
  1. func (engine *Engine) Handler() http.Handler {
  2.     if !engine.UseH2C {
  3.         return engine
  4.     }
  5.     h2s := &http2.Server{}
  6.     return h2c.NewHandler(engine, h2s)
  7. }
复制代码
这内里的h2c不是本次要关注的内容,这儿现实返回的是engine,那么engine就应该实现了下面的接口:
  1. type Handler interface {
  2.         ServeHTTP(ResponseWriter, *Request)
  3. }
复制代码
我们可以就接着看engine中ServeHTTP(...)方法的实现,如下所示:
  1. // ServeHTTP conforms to the http.Handler interface.
  2. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  3.     c := engine.pool.Get().(*Context)
  4.     c.writermem.reset(w)
  5.     c.Request = req
  6.     c.reset()
  7.     engine.handleHTTPRequest(c) // ref-3
  8.     engine.pool.Put(c)
  9. }
复制代码
现实处置处罚哀求的就是ref-3处的代码,我们继续往下看,该函数的实现如下所示:
  1. func (engine *Engine) handleHTTPRequest(c *Context) {
  2.         httpMethod := c.Request.Method
  3.         rPath := c.Request.URL.Path
  4.         unescape := false
  5.         if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
  6.                 rPath = c.Request.URL.RawPath
  7.                 unescape = engine.UnescapePathValues
  8.         }
  9.         if engine.RemoveExtraSlash {
  10.                 rPath = cleanPath(rPath)
  11.         }
  12.         // Find root of the tree for the given HTTP method
  13.         t := engine.trees
  14.         for i, tl := 0, len(t); i < tl; i++ { // 先找到匹配树中对应的根节点。
  15.                 if t[i].method != httpMethod {
  16.                         continue
  17.                 }
  18.                 root := t[i].root
  19.                 // Find route in tree
  20.                 value := root.getValue(rPath, c.params, c.skippedNodes, unescape) // ref-4
  21.                 if value.params != nil {
  22.                         c.Params = *value.params // ref-5
  23.                 }
  24.                 if value.handlers != nil {
  25.                         c.handlers = value.handlers
  26.                         c.fullPath = value.fullPath
  27.                         c.Next()
  28.                         c.writermem.WriteHeaderNow()
  29.                         return
  30.                 }
  31.                 if httpMethod != http.MethodConnect && rPath != "/" {
  32.                         if value.tsr && engine.RedirectTrailingSlash {
  33.                                 redirectTrailingSlash(c)
  34.                                 return
  35.                         }
  36.                         if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
  37.                                 return
  38.                         }
  39.                 }
  40.                 break
  41.         }
  42.         ...... // 省略其他代码
  43. }
复制代码
gin现实上会将注册的路由构成一个trie树结构,在网上找了一个gin为路由构建的树表示图,如下所示:

关于字符串搜索树trie可以看轻松搞懂Trie树,这篇文章中有详细的表明。
我们继续回到到代码中,上面的代码会起首找到http哀求方法对应的树根(root),然后在root下面的树中找到对应的节点(value),而且在ref-5处将节点的params值赋值给域对象中的Params。我们把关注点会集在ref-4中,接着往下看:
  1. // Returns the handle registered with the given path (key). The values of
  2. // wildcards are saved to a map.
  3. // If no handle can be found, a TSR (trailing slash redirect) recommendation is
  4. // made if a handle exists with an extra (without the) trailing slash for the
  5. // given path.
  6. func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
  7.         var globalParamsCount int16
  8. walk: // Outer loop for walking the tree
  9.         for {
  10.                 prefix := n.path
  11.                 if len(path) > len(prefix) {
  12.                         if path[:len(prefix)] == prefix {
  13.                                 path = path[len(prefix):]
  14.                                 // Try all the non-wildcard children first by matching the indices
  15.                                 idxc := path[0]
  16.                                 for i, c := range []byte(n.indices) {
  17.                                         if c == idxc {
  18.                                                 //  strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
  19.                                                 if n.wildChild {
  20.                                                         ...... // 省略其他的代码
  21.                                                 }
  22.                                                 n = n.children[i]
  23.                                                 continue walk
  24.                                         }
  25.                                 }
  26.                                 if !n.wildChild {
  27.                                         ...... // 省略其他代码
  28.                                 }
  29.                                 // Handle wildcard child, which is always at the end of the array
  30.                                 n = n.children[len(n.children)-1]
  31.                                 globalParamsCount++
  32.                                 switch n.nType {
  33.                                 case param: // 处理url路径中的占位符参数
  34.                                         // fix truncate the parameter
  35.                                         // tree_test.go  line: 204
  36.                                         // Find param end (either '/' or path end)
  37.                                         end := 0
  38.                                         for end < len(path) && path[end] != '/' {
  39.                                                 end++
  40.                                         }
  41.                                         // Save param value
  42.                                         if params != nil && cap(*params) > 0 {
  43.                                                 if value.params == nil {
  44.                                                         value.params = params
  45.                                                 }
  46.                                                 // Expand slice within preallocated capacity
  47.                                                 i := len(*value.params)
  48.                                                 *value.params = (*value.params)[:i+1]
  49.                                                 val := path[:end]
  50.                                                 if unescape {
  51.                                                         if v, err := url.QueryUnescape(val); err == nil {
  52.                                                                 val = v
  53.                                                         }
  54.                                                 }
  55.                           // 将路径中的占位符参数的key和value组装到params中
  56.                                                 (*value.params)[i] = Param{
  57.                                                         Key:   n.path[1:],
  58.                                                         Value: val,
  59.                                                 }
  60.                                         }
  61.                                         // we need to go deeper!
  62.                                         if end < len(path) {
  63.                                                 if len(n.children) > 0 {
  64.                                                         path = path[end:]
  65.                                                         n = n.children[0]
  66.                                                         continue walk
  67.                                                 }
  68.                                                 // ... but we can't
  69.                                                 value.tsr = len(path) == end+1
  70.                                                 return
  71.                                         }
  72.                                         if value.handlers = n.handlers; value.handlers != nil {
  73.                                                 value.fullPath = n.fullPath
  74.                                                 return
  75.                                         }
  76.                                         if len(n.children) == 1 {
  77.                                                 // No handle found. Check if a handle for this path + a
  78.                                                 // trailing slash exists for TSR recommendation
  79.                                                 n = n.children[0]
  80.                                                 value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
  81.                                         }
  82.                                         return
  83.                 case catchAll: // 处理URL路径中的通配符参数
  84.                                         ...... // 省略其他的代码
  85.                                 default:
  86.                                         panic("invalid node type")
  87.                                 }
  88.                         }
  89.                 }
  90.                 if path == prefix {
  91.                         // If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node
  92.                         // the current node needs to roll back to last valid skippedNode
  93.                         if n.handlers == nil && path != "/" {
  94.                                 for length := len(*skippedNodes); length > 0; length-- {
  95.                                         skippedNode := (*skippedNodes)[length-1]
  96.                                         *skippedNodes = (*skippedNodes)[:length-1]
  97.                                         if strings.HasSuffix(skippedNode.path, path) {
  98.                                                 path = skippedNode.path
  99.                                                 n = skippedNode.node
  100.                                                 if value.params != nil {
  101.                                                         *value.params = (*value.params)[:skippedNode.paramsCount]
  102.                                                 }
  103.                                                 globalParamsCount = skippedNode.paramsCount
  104.                                                 continue walk
  105.                                         }
  106.                                 }
  107.                                 //        n = latestNode.children[len(latestNode.children)-1]
  108.                         }
  109.                         // We should have reached the node containing the handle.
  110.                         // Check if this node has a handle registered.
  111.                         if value.handlers = n.handlers; value.handlers != nil {
  112.                                 value.fullPath = n.fullPath
  113.                                 return
  114.                         }
  115.                         // If there is no handle for this route, but this route has a
  116.                         // wildcard child, there must be a handle for this path with an
  117.                         // additional trailing slash
  118.                         if path == "/" && n.wildChild && n.nType != root {
  119.                                 value.tsr = true
  120.                                 return
  121.                         }
  122.                         if path == "/" && n.nType == static {
  123.                                 value.tsr = true
  124.                                 return
  125.                         }
  126.                         // No handle found. Check if a handle for this path + a
  127.                         // trailing slash exists for trailing slash recommendation
  128.                         for i, c := range []byte(n.indices) {
  129.                                 if c == '/' {
  130.                                         n = n.children[i]
  131.                                         value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
  132.                                                 (n.nType == catchAll && n.children[0].handlers != nil)
  133.                                         return
  134.                                 }
  135.                         }
  136.                         return
  137.                 }
  138.                 // Nothing found. We can recommend to redirect to the same URL with an
  139.                 // extra trailing slash if a leaf exists for that path
  140.                 value.tsr = path == "/" ||
  141.                         (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
  142.                                 path == prefix[:len(prefix)-1] && n.handlers != nil)
  143.                 // roll back to last valid skippedNode
  144.                 if !value.tsr && path != "/" {
  145.                         for length := len(*skippedNodes); length > 0; length-- {
  146.                                 skippedNode := (*skippedNodes)[length-1]
  147.                                 *skippedNodes = (*skippedNodes)[:length-1]
  148.                                 if strings.HasSuffix(skippedNode.path, path) {
  149.                                         path = skippedNode.path
  150.                                         n = skippedNode.node
  151.                                         if value.params != nil {
  152.                                                 *value.params = (*value.params)[:skippedNode.paramsCount]
  153.                                         }
  154.                                         globalParamsCount = skippedNode.paramsCount
  155.                                         continue walk
  156.                                 }
  157.                         }
  158.                 }
  159.                 return
  160.         }
  161. }
复制代码
上面的代码大抵分为三个步调,(1)通过匹配路径找到对应的节点;(2)判断节点的范例;(3)如果节点范例是param,也就是路径参数,那么就会对现实http哀求的路径举行剖析,组装键值对,而且把组装好的键值对赋值给params中。这个就和前面从Param获取参数连起来了。
二、gin是怎样剖析URL查询参数的?

仍然是从官网给出的示例步调代码开始看起:
  1. func main() {
  2.     router := gin.Default()
  3.     // 使用现有的基础请求对象解析查询字符串参数。
  4.     // 示例 URL: /welcome?firstname=Jane&lastname=Doe
  5.     router.GET("/welcome", func(c *gin.Context) {
  6.         firstname := c.DefaultQuery("firstname", "Guest")
  7.         // ref-6
  8.         lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") 的一种快捷方式
  9.         c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
  10.     })
  11.     router.Run(":8080")
  12. }
复制代码
ref-6处代码调用了Query的代码,如下所示:
  1. func (c *Context) Query(key string) (value string) {
  2.         value, _ = c.GetQuery(key)
  3.         return
  4. }
  5. func (c *Context) GetQuery(key string) (string, bool) {
  6.         if values, ok := c.GetQueryArray(key); ok {
  7.                 return values[0], ok
  8.         }
  9.         return "", false
  10. }
  11. func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
  12.         c.initQueryCache()
  13.         values, ok = c.queryCache[key] // 从这儿可以看到是从queryCache中获取的查询参数
  14.         return
  15. }
复制代码
上述代码的关键就在于c.initQueryCache()了,我们接着往下看:
  1. func (c *Context) initQueryCache() {
  2.         if c.queryCache == nil {
  3.                 if c.Request != nil {
  4.                         c.queryCache = c.Request.URL.Query()  // ref-7
  5.                 } else {
  6.                         c.queryCache = url.Values{}
  7.                 }
  8.         }
  9. }
复制代码
ref-7的代码展示了queryCache的泉源,细节如下所示:
  1. func (u *URL) Query() Values {
  2.         v, _ := ParseQuery(u.RawQuery)
  3.         return v
  4. }
复制代码
留意,从这儿开始已经进入到了go的net包下的url包了。我们继续看ParseQuery函数:
  1. func ParseQuery(query string) (Values, error) {
  2.         m := make(Values)
  3.         err := parseQuery(m, query)
  4.         return m, err
  5. }
  6. func parseQuery(m Values, query string) (err error) {
  7.         for query != "" {
  8.                 var key string
  9.                 key, query, _ = strings.Cut(query, "&") // 把query用&符号切开
  10.                 if strings.Contains(key, ";") {
  11.                         err = fmt.Errorf("invalid semicolon separator in query")
  12.                         continue
  13.                 }
  14.                 if key == "" {
  15.                         continue
  16.                 }
  17.                 key, value, _ := strings.Cut(key, "=") // 把key用=切开
  18.                 key, err1 := QueryUnescape(key)
  19.                 if err1 != nil {
  20.                         if err == nil {
  21.                                 err = err1
  22.                         }
  23.                         continue
  24.                 }
  25.                 value, err1 = QueryUnescape(value)
  26.                 if err1 != nil {
  27.                         if err == nil {
  28.                                 err = err1
  29.                         }
  30.                         continue
  31.                 }
  32.                 m[key] = append(m[key], value) // 组装key和value数据
  33.         }
  34.         return err
  35. }
复制代码
实在上面这段代码比力的简朴,利用本地断点调试就可以看出来运行过程了,重要是解释strings.Cut(...)函数的利用了。
三、总结

对于URL路径参数,gin会利用为路由构建的trie树来举行匹配,匹配到详细的节点后就会将现实http哀求路径中的参数提取出来,并组装为key-value对,赋值给域对象的Params中。
对于URL查询参数,则是直接调用go自带库中的net包举行剖析,详细剖析过程就是利用strings.Cut(...)函数举行切割,末了组装好键值对放在缓存中。
参考资料:
1、Gin 源码学习(一)丨哀求中 URL 的参数是怎样剖析的?
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表