Passing Context to Interface Methods

后端 未结 2 1251
暖寄归人
暖寄归人 2021-02-08 23:08

Somewhat inspired by this article last week, I\'m toying with refactoring an application I have to more explicitly pass context (DB pools, session stores, etc) to my handlers.

2条回答
  •  心在旅途
    2021-02-08 23:45

    After some discussion with a couple of helpful Gophers on #go-nuts, the method above is about "as good as it gets" from what I can discern.

    • The "con" with this method is that we pass a reference to our context struct twice: once as a pointer receiver in our method, and again as a struct member so ServeHTTP can access it as well.
    • The "pro" is that we can extend our struct type to accept a request context struct if we wanted to do so (like gocraft/web does).

    Note that we can't define our handlers as methods on appHandler i.e. func (ah *appHandler) IndexHandler(...) because we need to call the handler in ServeHTTP (i.e. ah.h(w,r)).

    type appContext struct {
        db        *sqlx.DB
        store     *sessions.CookieStore
        templates map[string]*template.Template
    }
    
    type appHandler struct {
        handler func(w http.ResponseWriter, r *http.Request) (int, error)
        *appContext // Embedded so we can just call app.db or app.store in our handlers.
    }
    
    // In main() ...
    context := &appContext{db: nil, store: nil}
    r.Get("/", appHandler{context.IndexHandler, context}) 
    ...
    

    This is also, most importantly, fully compatible with http.Handler so we can still wrap our handler struct with generic middleware like so: gzipHandler(appHandler{context.IndexHandler, context}).

    (I'm still open to other suggestions however!)


    Update

    Thanks to this great reply on Reddit I was able to find a better solution that didn't require passing two references to my context instance per-request.

    We instead just create a struct that accepts an embedded context and our handler type, and we still satisfy the http.Handler interface thanks to ServeHTTP. Handlers are no longer methods on our appContext type but instead just accept it as a parameter, which leads to a slightly longer function signature but is still "obvious" and easy to read. If we were concerned about 'typing' we're breaking even because we no longer have a method receiver to worry about.

    type appContext struct {
        db    *sqlx.DB
        store *sessions.CookieStore
        templates map[string]*template.Template
    
    type appHandler struct {
        *appContext
        h func(a *appContext, w http.ResponseWriter, r *http.Request) (int, error)
    }
    
    func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        // We can now access our context in here.
        status, err := ah.h(ah.appContext, w, r)
        log.Printf("Hello! DB: %v", ah.db)
        if err != nil {
            log.Printf("HTTP %d: %q", status, err)
            switch status {
            case http.StatusNotFound:
                // err := ah.renderTemplate(w, "http_404.tmpl", nil)
                http.NotFound(w, r)
            case http.StatusInternalServerError:
                // err := ah.renderTemplate(w, "http_500.tmpl", nil)
                http.Error(w, http.StatusText(status), status)
            default:
                // err := ah.renderTemplate(w, "http_error.tmpl", nil)
                http.Error(w, http.StatusText(status), status)
            }
        }
    }
    
    func main() {
        context := &appContext{
            db:    nil,
            store: nil,
            templates: nil,
        }
    
        r := web.New()
        // We pass a reference to context *once* per request, and it looks simpler
        r.Get("/", appHandler{context, IndexHandler})
    
        graceful.ListenAndServe(":8000", r)
    }
    
    func IndexHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, error) {
        fmt.Fprintf(w, "db is %q and store is %q\n", a.db, a.store)
        return 200, nil
    }
    

提交回复
热议问题