- Начало работы с Golang Web (1): понимание Http-сервера сверху донизу
- Начало работы с Golang Web (2): как реализовать маршрутизацию RESTful
- Начало работы с Golang Web (3): как элегантно спроектировать промежуточное ПО
- Начало работы с Golang Web (4): как разработать API
Резюме
существуетпредыдущий пост, мы уже можем реализовать высокопроизводительную маршрутизацию в стиле RESTful. Однако при разработке веб-приложений нам также нужны некоторые функции, которые можно расширить.
Поэтому в процессе проектирования фреймворка должны быть места для расширения, такие как: логирование, восстановление после сбоев и другие функции, если всю эту бизнес-логику заложить вController
/Handler
, код будет казаться особенно избыточным и беспорядочным.
Итак, в этой статье давайте рассмотрим, как более элегантно спроектировать промежуточное ПО.
1 Как осуществить сопряжение
Например, если мы хотим реализовать функцию ведения журнала, мы можем использовать этопросто и грубоПуть:
package main
import (
"fmt"
"net/http"
"time"
)
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
record(r.URL.Path)
fmt.Fprintf(w, "Hello World !")
}
func main() {
http.HandleFunc("/hello", helloWorldHandler)
http.ListenAndServe(":8000", nil)
}
func record(path string) {
fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + path)
}
Если мы это сделаем, мы действительно достигнем своей цели и зарегистрируем доступ.
Однако это совсем не элегантно.
КаждыйHandler
нужно звонить внутриrecord函数
, а затем запишитеpath
передается как параметрrecord函数
середина.
Если мы сделаем это, независимо от того, какую дополнительную функцию нам нужно добавить, мы должны жестко связать эту дополнительную функцию с нашей бизнес-логикой и не сможем добиться разделения между функциями расширения и бизнес-логикой.
2 Отделение записи от реализации
Поскольку в приведенной выше реализации ведение журнала и бизнес-реализация полностью связаны друг с другом, можем ли мы отделить их бизнес-реализацию?
Посмотрите на этот код:
func record(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
method := r.Method
fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
record(w ,r)
fmt.Fprintf(w, "Hello World !")
}
Здесь мы частично разделили бизнес-реализацию и ведение журнала.
Нам нужно только назвать бизнес-кодrecord(w,r)
функция, передающая содержимое запроса в качестве параметраrecord函数
в , то вrecord
Войдите в систему этим методом. В это время мы можем произвольно обрабатывать запрос внутри метода и сохранять данные, такие как путь запроса, метод запроса и т. д. И этот процесс,Прозрачность для внедрения в бизнес.
Таким образом, нам нужно иметь дело только с бизнес-логикойHandler
Функция вызывается в , а затем передаются параметры. Конкретная реализация этой функцииНе имеет ничего общего с бизнес-логикойиз.
Итак, есть ли способ объединить бизнес-логику и расширить функциональность?полностью отдельный, чтобы в бизнес-коде был только бизнес-код, чтобы код был чище? Давайте посмотрим вниз.
3 Промежуточное ПО для проектирования
мы впредыдущий постВнутри, проанализированоhttprouterРеализация этого пакета. Итак, мы идем прямо к нему, модифицируем его код, добавляемAddBeforeHandle
способ сделать этот маршрут расширяемым.
Обратите внимание, что метод AddBeforeHandle здесь написан самим автором, а конкретный процесс реализации можно увидеть позже.
3.1 Эффекты
Перед этим давайте посмотрим на эффект:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
func Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Hello World!\n")
}
func record(w http.ResponseWriter, r *http.Request){
path := r.URL.Path
method := r.Method
fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}
func main() {
router := httprouter.New()
router.AddBeforeHandle(record)
router.GET("/hello", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}
Эта часть кода почти точно такая же, как и предыдущая. Также создайте маршрут, который будет/hello
этот путь иHello
Этот обработчик связан вGET
в этом дереве префиксов, а затем начать прослушивание порта 8080.
Здесь важнееmain方法
Внутри второй строки:
router.AddBeforeHandle(record)
Как видно из названия метода, этот метод добавляет перед Handle процедуру обработки.
Давайте еще раз посмотрим на параметры, это метод, который мы упоминали выше для записи журнала доступа, Этот метод записывает запрос.URL
, метод запроса и время.
и в нашемHello(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
Никакой другой бизнес-логики функция не содержит.
В это время этоHandler
ТолькосконцентрируйсяЗаймитесь бизнес-логикой, как и всем остальным,Дайте это другой функции для реализации. Таким образом, завершитьразъединение.
Рассмотрим конкретный процесс реализации:
3.2 Конкретная реализация
Первый взглядAddBeforeHandle
Сюда:
func (r *Router) AddBeforeHandle(fn func(w http.ResponseWriter, req *http.Request)) {
r.beforeHandler = fn
}
Этот метод очень прост, то есть он получает параметр типа процессора и присваивает егоRouter
поля вbeforeHandler
.
Это называетсяbeforeHandler
Поле также добавлено нами вRouter
В, я думаю, вы можете видеть это, так называемыйAddBeforeHandle
Метод состоит в том, чтобы сохранить функцию обработки, которую мы передали вRouter
, позвоните ему, когда это необходимо.
Итак, давайте посмотрим, когда этот метод будет вызван. Перечисленный ниже метод, упомянутый в предыдущей статье, касаетсяhttprouter
Как осуществляется маршрутизация:
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
if root := r.trees[req.Method]; root != nil {
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
if r.beforeHandler != nil{
r.beforeHandler(w, req)
}
if ps != nil {
handle(w, req, *ps)
r.putParams(ps)
} else {
handle(w, req, nil)
}
return
}
}
...
}
Обратите внимание, что перед тем, как маршрутизатор нашел обработчик и был готов к выполнению, мы добавили следующие строки:
if r.beforeHandler != nil{
r.beforeHandler(w, req)
}
То есть, если мы ранее вызывалиAddBeforeHandle
метод, дающийbeforeHandler
Этому полю присваивается значение, тогда он не будетnil
, а затем вызовите эту функцию. Это также достигает нашей цели, выполнить функцию, которую мы установили перед обработкой запроса.
3.3 Мышление
Сейчас мы внедрилиполностью развязанныйпромежуточное ПО. Более того, это промежуточное ПО может быть настроено произвольно. Вы можете использовать его для ведения журнала, проверки разрешений и т. д., и эти функции не повлияют на бизнес-логику в Handler.
Если вы разработчик Java, вы можете подумать, что это похоже наFilter
,илиAOP
.
Однако, в отличие от фильтров, мы можем обрабатывать не только до прихода запроса, но и после завершения запроса. Например, этот запрос бывает несколькоpanic
, вы можете обрабатывать его в конце, или вы можете записать время этого запроса и т. д., все, что вам нужно сделать, это простоHandle
После метода вызовите метод, который вы зарегистрировали.
Например:
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
if root := r.trees[req.Method]; root != nil {
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
if r.beforeHandler != nil{
r.beforeHandler(w, req)
}
if ps != nil {
handle(w, req, *ps)
r.putParams(ps)
} else {
handle(w, req, nil)
}
if r.afterHandler != nil {
r.afterHandler(w, req)
}
return
}
}
...
}
мы только что добавилиafterHandler
Метод настолько прост.
Итак, возникает вопрос:Теперь для таких операций обработки мы можем добавить только промежуточное ПО до и после запроса. Что, если мы захотим добавить столько промежуточного программного обеспечения, сколько захотим?
Вы можете сначала подумать об этом сами, а потом давайте взглянем наgin
, как это достигается.
промежуточное ПО 4 Джин
4.1 Использование
Как мы все знаем, прежде чем читать исходный код, вы должны сначала увидеть, как он используется:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func Hello(ctx *gin.Context) {
fmt.Fprint(ctx.Writer, "Hello World!\n")
}
func main() {
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
router.GET("/hello", Hello)
router.Run(":8080")
}
Видно, что вgin
, метод использования промежуточного ПО аналогичен тому, что мы разработали выше. И бизнес-программа, и ПО промежуточного слоя полностью отделены друг от друга и добавляются при регистрации маршрутизации.
Но мы заметили, что вgin
нет различияHandle
доHandle
После. Итак, как он это сделал, давайте посмотрим на исходный код.
4.2 Объяснение исходного кода
Начнем с метода Use:
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
Здесь нам все равноgroup
Эта штука, он же группа маршрутизации, к нашей статье отношения не имеет, его мы сначала проигнорируем. нам просто нужно увидетьappend
метод. Метод Use заключается в добавлении всех функций в параметры кgroup.Handlers
середина. здесьgroup.Handlers
,ЯвляетсяHandler
массив типов.
Таким образом, в джине каждое промежуточное ПО такжеHandler
Тип.
В предыдущем разделе мы оставили вопрос,Как реализовать несколько промежуточных программ. Ответ здесь, используйтемножествоспасти.
Затем снова возникает проблема:Как обеспечить порядок звонков?
Продолжим смотреть на регистрацию маршрутов:
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
Здесь тоже что-то знакомое? как упоминалось в предыдущей статьеhttprouter
очень похоже, мы смотрим прямо наgroup.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()
}
В этом коде первая строка оpath
Давайте проигнорируем это, это также связано с группировкой маршрутизации, короче говоря, это сращивание полного запроса.path
.
Сначала посмотрите на вторую строку, имя методаcombineHandlers
, мы можем догадаться о роли этого метода и объединить различные обработчики. Ознакомьтесь с подробным кодом:
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
Сначала объясните, вот возвратHandlersChain
тип, даHandler
массив .
То есть в этом методе промежуточное ПО, ранее помещенное в группу, и обработчик текущего маршрута объединяются в новый массив.
И промежуточное ПО находится спереди, а обработчик маршрутизации — сзади.Обратите внимание, что этот порядок важен.
Затем мы продолжаем вниз, и после выполнения этого метода выполняется следующее:addRoute
метод. Не буду говорить об этом здесь. Итак, самое главное, что все промежуточное ПО и обработчик здесь.сгруппированы вместе, привязанный к этому префиксному дереву.
На этом с регистрационным аспектом покончено, посмотрим, как он с этим справится.Последовательность вызова каждого промежуточного ПО.
Поскольку наша цель — увидеть, как маршрут обрабатывает запросы, давайте посмотрим непосредственно наgin
изServeHTTP
метод:
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)
}
Здесь следует отметить, что*Context
, который инкапсулирует запрос, включаяresponseWriter
,*http.Request
Ждать.
Давайте продолжать смотреть внизhandleHTTPRequest(c)
Сюда:
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
...
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
}
...
}
...
}
В этом методе, по сути, он такой же, как тот, который мы изучали ранееhttprouter
очень похожи. также на основе请求方法
Найдите соответствующее дерево префиксов, а затем получите соответствующийHandler
, и положить полученноеhandler
массив хранится вContext
середина.
Здесь мы обращаем внимание на метод c.Next(), который является промежуточным вызовом в gin.самый изысканныйчасть. Давайте взглянем:
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
Мы можем видеть, что при вызове этогоNext()
метод, он будет добавлен для сохранения вContext
Нижний индекс в нижнем индексе, а затем выполнить в соответствии с порядком этого нижнего индексаhandler
.
Как мы упоминали ранее, мы разместили промежуточное программное обеспечение в этомhandler
Перед массивом в первую очередь выполняется промежуточное ПО, а затем в последнюю очередь выполняется пользовательская настройка.handler
.
Давайте еще раз взглянем на промежуточное ПО ведения журнала:
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
...
return func(c *Context) {
//开始计时
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
c.Next()
...
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
...
}
}
Как видите, сначала запускается отсчет времени, а потом идет звонокc.Next()
На этом методе и только тогда заканчивается отсчет времени.
Тогда мы можем сделать вывод из этого,c.Next()
Код позади определяется пользователем после выполненияHandler
только что казненный.
То есть, по сути, бизнес-логика мидлвара такова:
func Middleware(c *gin.Context){
//请求前执行
c.Next()
//请求后执行
}
5 в конце
Прежде всего, спасибо, что вы здесь.
Проще говоря, мы должны рассмотретьРазвязка, чтобы бизнес-код мог сосредоточиться на бизнесе, а промежуточное ПО — на реализации функций. Чтобы добиться этого, мы можем изменить логику реализации маршрута для выполненияHandler
До и после добавления вызовов промежуточного ПО.
В этой статье может быть много упущений. Если в процессе чтения будут какие-то пояснения не к месту, или будут какие-то ошибки в понимании автора, пожалуйста, оставьте сообщение, чтобы меня поправили.
Еще раз спасибо~
PS: Если у вас есть другие вопросы, вы также можете найти автора на официальном аккаунте. Кроме того, все статьи будут обновлены в общедоступном аккаунте как можно скорее, добро пожаловать, чтобы поиграть с автором~