Изучение исходного кода Gin (1) 丨 Как разобрать параметры URL в запросе?

Go
Изучение исходного кода Gin (1) 丨 Как разобрать параметры URL в запросе?

If you need performance and good productivity, you will love Gin.

Это первая статья по изучению исходного кода Gin. Почему именно Gin?

Как указано в официальной документации Gin, Gin — это веб-фреймворк, ориентированный на производительность и производительность, и утверждает, что производительность почти в 40 раз выше, чем у httprouter.Это одна из причин выбора Gin в качестве исходного кода для изучения, поскольку он ориентирован производительность, за которой следует встроенная библиотека функций Gonetбиблиотека иcontextБиблиотека, если вы хотите сказать, почему Go так популярен в Китае, то причина должна быть той же, что иnetбиблиотека иcontextбиблиотеки, поэтому в статьях этой серии будут использованы преимуществаnetбиблиотека иcontextПрименение библиотеки в Gin, две библиотеки будут объясняться в соответствии с тенденцией.

Эта серия статей пойдет от простого к сложному, от простого к сложному В процессе объяснения исходного кода Gin в сочетании с собственной библиотекой функций Go будут объяснены некоторые оригинальные конструкции в собственной библиотеке функций Go.

Приступим к первой части изучения исходного кода Gin: как анализируются параметры URL-адреса в запросе?

содержание

Разрешение параметров в путях

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

	router.GET("/user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		c.String(http.StatusOK, "%s is %s", name, action)
	})

	router.Run(":8000")
}

Приводя пример из официальной документации Gin, акцентируем внимание наc.Param(key)функция выше.

Когда инициируется запрос GET с URI /user/cole/send, полученное тело ответа выглядит следующим образом:

cole is /send

Когда инициируется запрос GET с URI /user/cole/, тело ответа выглядит следующим образом:

cole is /

Внутри Джин, как это делается? Давайте сначала понаблюдаемgin.Contextвнутренняя функцияParam(), его исходный код выглядит следующим образом:

// Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key)
//     router.GET("/user/:id", func(c *gin.Context) {
//         // a GET request to /user/john
//         id := c.Param("id") // id == "john"
//     })
func (c *Context) Param(key string) string {
	return c.Params.ByName(key)
}

Из комментариев в исходном коде известно, чтоc.Param(key)функция действительно простоc.Params.ByName()Ярлык для функции, тогда давайте посмотрим на нее еще разc.ParamsЧто такое сакральное свойство и его тип, его исходный код выглядит следующим образом:

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
	Params Params
}

// Param is a single URL parameter, consisting of a key and a value.
type Param struct {
	Key   string
	Value string
}

// Params is a Param-slice, as returned by the router.
// The slice is ordered, the first URL parameter is also the first slice value.
// It is therefore safe to read values by the index.
type Params []Param

Во-первых,Paramsдаgin.ContextПараметр в типе (некоторые атрибуты опущены в исходном коде выше),gin.ContextЭто самая важная часть Gin, и ее роль аналогична роли в собственной библиотеке Go.contextБиблиотеки, которые будут объяснены отдельно в последующих статьях этой серии.

тогда,Paramsтип - этоrouterвозвращениеParamСлайс, между тем, слайс упорядочен, первый параметр URL также является первым значением слайса, иParamТип сделанKeyиValueиспользуется для представления параметров в URL.

Итак, приведенное выше получает URL-адрес вnameпараметры иactionПараметры также можно получить с помощью следующих методов:

name := c.Params[0].Value
action := c.Params[1].Value

И это не то, о чем мы заботимся. Вопрос, который мы хотим знать, заключается в том, как Gin внутренне передает параметры в URL-адресе вc.Paramsсередина? Взгляните на код ниже:

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

	router.GET("/aa", func(c *gin.Context) {})
	router.GET("/bb", func(c *gin.Context) {})
	router.GET("/u", func(c *gin.Context) {})
	router.GET("/up", func(c *gin.Context) {})

	router.POST("/cc", func(c *gin.Context) {})
	router.POST("/dd", func(c *gin.Context) {})
	router.POST("/e", func(c *gin.Context) {})
	router.POST("/ep", func(c *gin.Context) {})

	// http://127.0.0.1:8000/user/cole/send => cole is /send
	// http://127.0.0.1:8000/user/cole/ => cole is /
	router.GET("/user/:name/*action", func(c *gin.Context) {
		// name := c.Param("name")
		// action := c.Param("action")

		name := c.Params[0].Value
		action := c.Params[1].Value
		c.String(http.StatusOK, "%s is %s", name, action)
	})

	router.Run(":8000")
}

Сосредоточьтесь на привязке маршрутов.Этот код сохраняет первый маршрут GET и создает 4 маршрута GET и 4 маршрута POST.Внутри Gin будет сгенерировано что-то вроде показанного ниже дерева маршрутизации.

Конечно, вопрос о том, как сопоставлен URL-адрес запроса, не является центром этой статьи. Это будет подробно объяснено в последующих статьях. Здесь нам нужно сосредоточиться на узлах вwildChildСтоимость свойстваtrueузел. В сочетании с приведенным выше рисунком взгляните на следующий код (некоторые исходные коды опущены для акцента):

func (engine *Engine) handleHTTPRequest(c *Context) {
	httpMethod := c.Request.Method
	rPath := c.Request.URL.Path
    unescape := false
    ...
    ...

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
        ...
        ...
	}
    ...
    ...
}

Во-первых, метод получения запроса и URL-адрес запроса с указанным вышеhttp://127.0.0.1:8000/user/cole/sendЗапрос, например,httpMethodиrPathсоответственноGETи/user/cole/send.

Затем используйтеengine.treesПолучите срез дерева маршрутизации (верхняя часть диаграммы дерева маршрутизации выше) и пройдите по срезу через цикл for, чтобы найти тип с помощьюhttpMethodКорневой узел того же дерева маршрутизации.

Наконец, вызовите корневой узелgetValue(path, po, unescape)функция, которая возвращаетnodeValueобъект типа,paramsЗначение атрибутаc.Params.

Итак, наше внимание переключилось наgetValue(path, po, unescape)функция,unescapeПараметр используется, чтобы отметить, нужно ли экранировать или нет, и здесь он будет проигнорирован.Исходный код ниже показываетgetValue(path, po, unescape)Процесс анализа параметров URL в функции также сохраняет только исходный код, относящийся к содержанию этой статьи:

func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
	value.params = po
walk: // Outer loop for walking the tree
	for {
		if len(path) > len(n.path) {
			if path[:len(n.path)] == n.path {
				path = path[len(n.path):]
				// 从根往下匹配, 找到节点中wildChild属性为true的节点
				if !n.wildChild {
					c := path[0]
					for i := 0; i < len(n.indices); i++ {
						if c == n.indices[i] {
							n = n.children[i]
							continue walk
						}
					}

					...
					...
					return
				}

				// handle wildcard child
				n = n.children[0]
				// 匹配两种节点类型: param和catchAll
				// 可简单理解为:
				// 节点的path值为':xxx', 则节点为param类型节点
				// 节点的path值为'/*xxx', 则节点为catchAll类型节点
				switch n.nType {
				case param:
					// find param end (either '/' or path end)
					end := 0
					for end < len(path) && path[end] != '/' {
						end++
					}

					// save param value
					if cap(value.params) < int(n.maxParams) {
						value.params = make(Params, 0, n.maxParams)
					}
					i := len(value.params)
					value.params = value.params[:i+1] // expand slice within preallocated capacity
					value.params[i].Key = n.path[1:]
					val := path[:end]
					if unescape {
						var err error
						if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
							value.params[i].Value = val // fallback, in case of error
						}
					} else {
						value.params[i].Value = val
					}

					// we need to go deeper!
					if end < len(path) {
						if len(n.children) > 0 {
							path = path[end:]
							n = n.children[0]
							continue walk
						}

						...
						return
					}
					...
					...
					return

				case catchAll:
					// save param value
					if cap(value.params) < int(n.maxParams) {
						value.params = make(Params, 0, n.maxParams)
					}
					i := len(value.params)
					value.params = value.params[:i+1] // expand slice within preallocated capacity
					value.params[i].Key = n.path[2:]
					if unescape {
						var err error
						if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
							value.params[i].Value = path // fallback, in case of error
						}
					} else {
						value.params[i].Value = path
					}
					return

				default:
					panic("invalid node type")
				}
			}
		}
		...
		...
		return
	}
}

Во-первых, черезpathСопоставьте в дереве маршрутов и найдите узел вwildChildзначениеtrueузел, указывающий, что дочерний узел этого узла является узлом с подстановочными знаками, а затем получить дочерний узел этого узла.

Затем используйте переключатель, чтобы определить тип подстановочного узла, если онparam, затем перехватите, получите ключ и значение параметра и поместите его вvalue.paramsв; еслиcatchAll, вам не нужно перехватывать, получить ключ и значение параметра напрямую, поместить его вvalue.paramsв. вn.maxParamsАтрибуты назначаются при создании маршрутов, на что здесь не нужно обращать внимание, и это будет рассмотрено в последующих статьях этой серии.

В приведенном выше коде более извилистая часть — это в основном сопоставление узлов, которое можно рассматривать в сочетании с приведенной выше диаграммой дерева маршрутизации, которая проста для понимания. также опущены.исходный код, не должно быть сложно.

Разбор параметров строк запроса

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

	// http://127.0.0.1:8000/welcome?firstname=Les&lastname=An => Hello Les An
	router.GET("/welcome", func(c *gin.Context) {
		firstname := c.DefaultQuery("firstname", "Guest")
		lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

		c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
	})
	router.Run(":8080")
}

Точно так же, приводя примеры в официальной документации Gin, акцентируем внимание наc.DefaultQuery(key, defaultValue)иc.Query(key)Ну, конечно, между ними нет никакой разницы.

Когда получение запроса с URI / Welcome? FirstName = LES & LASTNAME = AN инициируется, результат теста ответа выглядит следующим образом:

Hello Les An

Далее посмотрите наc.DefaultQuery(key, defaultValue)иc.Query(key)Исходный код:

// Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//     GET /path?id=1234&name=Manu&value=
// 	   c.Query("id") == "1234"
// 	   c.Query("name") == "Manu"
// 	   c.Query("value") == ""
// 	   c.Query("wtf") == ""
func (c *Context) Query(key string) string {
	value, _ := c.GetQuery(key)
	return value
}

// DefaultQuery returns the keyed url query value if it exists,
// otherwise it returns the specified defaultValue string.
// See: Query() and GetQuery() for further information.
//     GET /?name=Manu&lastname=
//     c.DefaultQuery("name", "unknown") == "Manu"
//     c.DefaultQuery("id", "none") == "none"
//     c.DefaultQuery("lastname", "none") == ""
func (c *Context) DefaultQuery(key, defaultValue string) string {
	if value, ok := c.GetQuery(key); ok {
		return value
	}
	return defaultValue
}

Как видно из приведенного выше исходного кода, оба вызываютc.GetQuery(key)function, далее проследим исходный код:

// GetQuery is like Query(), it returns the keyed url query value
// if it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns `("", false)`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//     GET /?name=Manu&lastname=
//     ("Manu", true) == c.GetQuery("name")
//     ("", false) == c.GetQuery("id")
//     ("", true) == c.GetQuery("lastname")
func (c *Context) GetQuery(key string) (string, bool) {
	if values, ok := c.GetQueryArray(key); ok {
		return values[0], ok
	}
	return "", false
}

// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) {
	c.getQueryCache()
	if values, ok := c.queryCache[key]; ok && len(values) > 0 {
		return values, true
	}
	return []string{}, false
}

существуетc.GetQuery(key)Вызывается внутри функцииc.GetQueryArray(key)функция, в то время какc.GetQueryArray(key)В функции первый вызовc.getQueryCache()функция, то вы можете пройтиkeyНепосредственно изc.queryCacheполучить соответствующийvalueценность, в основномc.getQueryCache()Функция функции состоит в том, чтобы сохранить параметры строки запроса вc.queryCacheсередина. Далее давайте посмотримc.getQueryCache()Исходный код функции:

func (c *Context) getQueryCache() {
	if c.queryCache == nil {
		c.queryCache = c.Request.URL.Query()
	}
}

судить первымc.queryCacheЯвляется ли значениеnil, еслиnil, затем позвонитеc.Request.URL.Query()функцию, иначе ничего не делать.

Мы ориентируемся наc.Requestвыше, это*http.Requestтип, расположенный в библиотеке net/http в собственной библиотеке Go, иc.Request.URLОн расположен в библиотеке net/url в собственной библиотеке функций Go, что указывает на то, что следующий исходный код исходит из собственной библиотеки функций Go. Давайте проследим исходный код:

// Query parses RawQuery and returns the corresponding values.
// It silently discards malformed value pairs.
// To check errors use ParseQuery.
func (u *URL) Query() Values {
	v, _ := ParseQuery(u.RawQuery)
	return v
}

// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string

// ParseQuery parses the URL-encoded query string and returns
// a map listing the values specified for each key.
// ParseQuery always returns a non-nil map containing all the
// valid query parameters found; err describes the first decoding error
// encountered, if any.
//
// Query is expected to be a list of key=value settings separated by
// ampersands or semicolons. A setting without an equals sign is
// interpreted as a key set to an empty value.
func ParseQuery(query string) (Values, error) {
	m := make(Values)
	err := parseQuery(m, query)
	return m, err
}

func parseQuery(m Values, query string) (err error) {
	for query != "" {
		key := query
		// 如果key中存在'&'或者';', 则用其对key进行分割
		// 例如切割前: key = firstname=Les&lastname=An
		// 例如切割后: key = firstname=Les, query = lastname=An
		if i := strings.IndexAny(key, "&;"); i >= 0 {
			key, query = key[:i], key[i+1:]
		} else {
			query = ""
		}
		if key == "" {
			continue
		}
		value := ""
		// 如果key中存在'=', 则用其对key进行分割
		// 例如切割前: key = firstname=Les
		// 例如切割后: key = firstname, value = Les
		if i := strings.Index(key, "="); i >= 0 {
			key, value = key[:i], key[i+1:]
		}
		// 对key进行转义处理
		key, err1 := QueryUnescape(key)
		if err1 != nil {
			if err == nil {
				err = err1
			}
			continue
		}
		// 对value进行转义处理
		value, err1 = QueryUnescape(value)
		if err1 != nil {
			if err == nil {
				err = err1
			}
			continue
		}
		// 将value追加至m[key]切片中
		m[key] = append(m[key], value)
	}
	return err
}

прежде всегоu.Query()функция, путем разбораRawQueryЗначение , взяв приведенный выше запрос GET в качестве примера, тогда егоRawQueryзначениеfirstname=Les&lastname=An, возвращаемое значениеValuesобъект типа,ValuesКарта, ключ которой — строка, а значение — фрагмент строк.

ПослеParseQuery(query)функция, в которой создается функцияValuesтип объектаmи используйте его и пропущенные вqueryв видеparseQuery(m, query)параметры функции.

Наконец вparseQuery(m, query)внутри функцииqueryрешитьсяm, на данный момент синтаксический анализ параметра строки запроса завершен.

Суммировать

В этой статье объясняются два способа анализа параметров URL в Gin, а именно анализ параметров в пути и анализ параметров строки запроса.

Среди них процесс анализа параметров в пути сочетает в себе механизм сопоставления маршрутов в Gin. Благодаря оригинальной конструкции механизма сопоставления маршрутов этот метод анализа параметров очень эффективен. Конечно, механизм сопоставления маршрутов немного сложен, что будет обсуждаться позже в этой серии.Это будет подробно объяснено в статье, затем синтаксический анализ параметров символов запроса, разница между этим методом синтаксического анализа параметров и собственной библиотекой функций Go net/url library заключается в том, что Gin сохраняет проанализированные параметры в контексте, так что если это так, при получении нескольких параметров нет необходимости повторно анализировать строку запроса, что значительно повышает эффективность получения нескольких параметров, что является одной из причин, почему Gin так эффективен.

На этом эта статья закончена. Спасибо за прочтение. В следующей статье из этой серии объясняется, как данные формы в запросе POST анализируются внутри Gin.