好的蛋糕是您可以轻松切成薄片且不会碎屑的蛋糕。 这就是这个项目的全部内容:3个简单的零件,没有讨厌的添加剂。 在第1部分和第2部分中,我解释了使用docker设置golang项目,创建可配置服务器,与DB交互,添加路由和处理程序的基础知识。
当多个处理程序不得不重用同一逻辑时会发生什么?
解决此问题的最优雅方法是什么?
这是中间件发挥作用的地方,它将是我在这里的主要重点。
在路由处理程序的上下文中使用中间件的想法是,我们从多个小功能(中间件)构造处理程序。 这样可以轻松实现代码的重用性,并使处理程序的推理变得更加容易。
我将通过向其添加更多逻辑来继续处理先前文章中的用户资源。 每个处理程序可能具有的一件事是请求日志记录。 在实现它之前,有一个小的实用程序函数可以帮助我构造处理程序:
// pkg/middleware/middleware.go
package middleware
import (
"github.com/julienschmidt/httprouter"
)
type Middleware func (httprouter.Handle) httprouter . Handle
func Chain (f httprouter.Handle, m ...Middleware) httprouter . Handle {
if len (m) == 0 {
return f
}
return m[ 0 ](Chain(f, m[ 1 :]...))
}
Chain
函数将接受任何数量的中间件类型的函数,并一一调用它们,从而使我们可以完全控制流程,稍后再进行介绍。 现在,我已经准备好了,我将添加简单的请求日志记录中间件。 由于它将在多个处理程序中重用,因此将其放在pkg / middleware目录中。 所有特定于处理程序的中间件都将位于处理程序本身旁边:
// pkg/middleware/logging.go
package middleware
import (
"net/http"
"github.com/boilerplate/pkg/logger"
"github.com/julienschmidt/httprouter"
)
func LogRequest (next httprouter.Handle) httprouter . Handle {
return func (w http.ResponseWriter, r *http.Request, p httprouter.Params) {
logger.Info.Printf( "%s - %s %s %s" , r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI())
next(w, r, p)
}
}
一个简单的高阶函数,接受并返回httprouter.Handle
类型的函数,并从两者之间的请求中读取一些数据。 接下来,我计划接受用户ID作为url参数,并且我想确保它是有效的数字字符串。 如果不是,我将仅用状态代码412响应客户端。听起来像这样的逻辑可以作为中间件实现:
// cmd/api/handlers/getuser/validarequest.go
package getuser
import (
"context"
"fmt"
"net/http"
"strconv"
"github.com/boilerplate/cmd/api/models"
"github.com/julienschmidt/httprouter"
)
func validateRequest (next httprouter.Handle) httprouter . Handle {
return func (w http.ResponseWriter, r *http.Request, p httprouter.Params) {
uid := p.ByName( "id" )
id, err := strconv.Atoi(uid)
if err != nil {
w.WriteHeader(http.StatusPreconditionFailed)
fmt.Fprintf(w, "malformed id" )
return
}
ctx := context.WithValue(r.Context(), models.CtxKey( "userid" ), id)
r = r.WithContext(ctx)
next(w, r, p)
}
}
与记录中间件相同的原理。 上下文是在中间件之间共享数据的一种标准方式,出于演示目的,我在其中添加了用户ID。 如果客户端发送格式错误的ID,我将跳过中间件链中的所有后续步骤,方法是不调用next
函数并直接从此处响应客户端。
下一步是getuser.Do
处理程序本身:
// cmd/api/handlers/getuser/getuser.go
package getuser
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/boilerplate/cmd/api/models"
"github.com/boilerplate/pkg/application"
"github.com/boilerplate/pkg/middleware"
"github.com/julienschmidt/httprouter"
)
func getUser (app *application.Application) httprouter . Handle {
return func (w http.ResponseWriter, r *http.Request, p httprouter.Params) {
defer r.Body.Close()
id := r.Context().Value(models.CtxKey( "userid" ))
user := &models.User{ID: id.( int )}
if err := user.GetByID(r.Context(), app); err != nil {
if errors.Is(err, sql.ErrNoRows) {
w.WriteHeader(http.StatusPreconditionFailed)
fmt.Fprintf(w, "user does not exist" )
return
}
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Oops" )
return
}
w.Header().Set( "Content-Type" , "application/json" )
response, _ := json.Marshal(user)
w.Write(response)
}
}
func Do (app *application.Application) httprouter . Handle {
mdw := []middleware.Middleware{
middleware.LogRequest,
validateRequest,
}
return middleware.Chain(getUser(app), mdw...)
}
我首先创建所有必需中间件的一部分,然后将其变成可变参数函数参数。 middleware.Chain
的实现方式是从右到左调用函数,因此首先将调用LogRequest ,然后是validateRequest和getUser 。
这里没有什么要指出的。 请注意, mdw slice中的中间件的签名与getUser的签名略有不同。 这是因为getUser是链中的最后一步,它不会调用next
函数来跳转到链的下一步。 第二件事是我如何从上下文中读取价值。 让我们在测试处理程序之前启动服务:
docker-compose up --build
现在,让我们以foobar作为用户ID进行请求。 由于foobar不是数字字符串,因此我希望响应中的id消息格式错误:
现在,让我们尝试有效的ID。 目前,我在用户表中没有ID为2
记录,因此希望用户不存在该消息:
您可以在这里找到整个项目。 我真的希望这对您来说是一个很好的起点,并有助于构建新的,令人兴奋的东西。
保持安全,吃蛋糕并保持编码!
From: https://hackernoon.com/how-to-create-golang-rest-api-project-layout-configuration-part-3-pr453ylt