ds3464 2014-07-12 15:51
浏览 44
已采纳

将上下文传递给接口方法

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.

However, one issue I'm having is that without a global templates map, the ServeHTTP method on my custom handler type (as to satisfy http.Handler) can no longer access the map to render a template.

I need to either retain the global templates variable, or re-define my custom handler type as a struct.

Is there a better way to achieve this?

func.go

package main

import (
    "fmt"
    "log"
    "net/http"

    "html/template"

    "github.com/gorilla/sessions"
    "github.com/jmoiron/sqlx"
    "github.com/zenazn/goji/graceful"
    "github.com/zenazn/goji/web"
)

var templates map[string]*template.Template

type appContext struct {
    db    *sqlx.DB
    store *sessions.CookieStore
}

type appHandler func(w http.ResponseWriter, r *http.Request) (int, error)

func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // templates must be global for us to use it here
    status, err := ah(w, r)
    if err != nil {
        log.Printf("HTTP %d: %q", status, err)
        switch status {
        case http.StatusNotFound:
            // Would actually render a "http_404.tmpl" here...
            http.NotFound(w, r)
        case http.StatusInternalServerError:
            // Would actually render a "http_500.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        default:
            // Would actually render a "http_error.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        }
    }
}

func main() {
    // Both are 'nil' just for this example
    context := &appContext{db: nil, store: nil}

    r := web.New()
    r.Get("/", appHandler(context.IndexHandler))
    graceful.ListenAndServe(":8000", r)
}

func (app *appContext) IndexHandler(w http.ResponseWriter, r *http.Request) (int, error) {
    fmt.Fprintf(w, "db is %q and store is %q", app.db, app.store)
    return 200, nil
}

struct.go

package main

import (
    "fmt"
    "log"
    "net/http"

    "html/template"

    "github.com/gorilla/sessions"
    "github.com/jmoiron/sqlx"
    "github.com/zenazn/goji/graceful"
    "github.com/zenazn/goji/web"
)

type appContext struct {
    db        *sqlx.DB
    store     *sessions.CookieStore
    templates map[string]*template.Template
}

// We need to define our custom handler type as a struct
type appHandler struct {
    handler func(w http.ResponseWriter, r *http.Request) (int, error)
    c       *appContext
}

func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    status, err := ah.handler(w, r)
    if err != nil {
        log.Printf("HTTP %d: %q", status, err)
        switch status {
        case http.StatusNotFound:
            // Would actually render a "http_404.tmpl" here...
            http.NotFound(w, r)
        case http.StatusInternalServerError:
            // Would actually render a "http_500.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        default:
            // Would actually render a "http_error.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        }
    }
}

func main() {
    // Both are 'nil' just for this example
    context := &appContext{db: nil, store: nil}

    r := web.New()
    // A little ugly, but it works.
    r.Get("/", appHandler{context.IndexHandler, context})
    graceful.ListenAndServe(":8000", r)
}

func (app *appContext) IndexHandler(w http.ResponseWriter, r *http.Request) (int, error) {
    fmt.Fprintf(w, "db is %q and store is %q", app.db, app.store)
    return 200, nil
}

Is there a cleaner way to pass the context instance to ServeHTTP?

Note that go build -gcflags=-m shows that neither option appears to be worse in teams of heap allocation: the &appContext literal escapes to the heap (as expected) in both cases, although my interpretation is that the struct-based option does pass a second pointer (to context) on each request—correct me if I'm wrong here as I'd love to get a better understanding of this.

I'm not wholly convinced that globals are bad in package main (i.e. not a lib) provided they are safe to use in that manner (read only/mutexes/a pool), but I do like clarity having to explicitly pass context provides.

  • 写回答

2条回答 默认 最新

  • douchuang4181 2014-07-14 01:27
    关注

    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
    ", a.db, a.store)
        return 200, nil
    }
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

悬赏问题

  • ¥15 C++使用Gunplot
  • ¥15 这个电路是如何实现路灯控制器的,原理是什么,怎么求解灯亮起后熄灭的时间如图?
  • ¥15 matlab数字图像处理频率域滤波
  • ¥15 在abaqus做了二维正交切削模型,给刀具添加了超声振动条件后输出切削力为什么比普通切削增大这么多
  • ¥15 ELGamal和paillier计算效率谁快?
  • ¥15 file converter 转换格式失败 报错 Error marking filters as finished,如何解决?
  • ¥15 Arcgis相交分析无法绘制一个或多个图形
  • ¥15 关于#r语言#的问题:差异分析前数据准备,报错Error in data[, sampleName1] : subscript out of bounds请问怎么解决呀以下是全部代码:
  • ¥15 seatunnel-web使用SQL组件时候后台报错,无法找到表格
  • ¥15 fpga自动售货机数码管(相关搜索:数字时钟)