【问题标题】:Passing Context to Interface Methods将上下文传递给接口方法
【发布时间】:2014-07-12 15:51:34
【问题描述】:

上周受到this article 的启发,我正在尝试重构一个应用程序,我必须更明确地将上下文(数据库池、会话存储等)传递给我的处理程序。

但是,我遇到的一个问题是,如果没有全局模板映射,我的自定义处理程序类型上的 ServeHTTP 方法(为了满足 http.Handler)不能再访问映射来呈现模板。

我需要保留全局 templates 变量,或者将我的自定义处理程序类型重新定义为结构。

有没有更好的方法来实现这一点?

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
}

有没有更简洁的方法将context 实例传递给ServeHTTP

请注意,go build -gcflags=-m 表明在堆分配团队中这两个选项似乎都没有更糟:&appContext 在这两种情况下都会逃逸到堆中(如预期的那样),尽管我的解释是基于结构的选项确实如此在每个请求上传递第二个指针(指向context)——如果我在这里错了,请纠正我,因为我很想更好地理解这一点。

我并不完全相信全局变量在 main 包(即不是 lib)中是不好的,前提是它们可以安全地以这种方式使用(只读/互斥体/池),但我确实希望明确通过上下文提供。

【问题讨论】:

    标签: go


    【解决方案1】:

    我会使用闭包并执行以下操作:

    func IndexHandler(a *appContext) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *httpRequest) {
            // ... do stuff
            fmt.Fprintf(w, "db is %q and store is %q\n", a.db, a.store)
        })
    }
    

    只需使用返回的http.Handler

    您只需确保您的 appContext 是 goroutine 安全的。

    【讨论】:

    • 谢谢。通常我会为此关闭,因为它们很容易实现,虽然有点重复。但是我需要满足两个接口:http.Handler web.Handler(前者加上附加的请求上下文)并且无法通过闭包来实现。
    • 你可以实现你自己的类似于http.HandlerFunc的类型来满足你所有的接口。
    • 这正是我所做的(根据我自己的回答):) appHandler 有一个 ServeHTTP 方法和一个(未显示)ServeHTTPC 方法来满足这两个接口。
    【解决方案2】:

    在#go-nuts 上与几位乐于助人的 Gophers 讨论后,据我所知,上述方法是“尽其所能”。

    • 这个方法的“缺点”是我们将一个引用传递给我们的上下文结构两次:一次作为我们方法中的指针接收器,另一次作为结构成员,以便ServeHTTP 可以访问它也是。
    • “优点”是我们可以扩展我们的结构类型以接受请求上下文结构,如果我们愿意的话(就像gocraft/web 那样)。

    请注意,我们不能将处理程序定义为appHandler 上的方法,即func (ah *appHandler) IndexHandler(...),因为我们需要调用ServeHTTP 中的处理程序(即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}) 
    ...
    

    最重要的是,这也与http.Handler 完全兼容,因此我们仍然可以使用通用中间件包装我们的处理程序结构,如下所示:gzipHandler(appHandler{context.IndexHandler, context})

    (不过,我仍然愿意接受其他建议!)


    更新

    感谢this great reply on Reddit,我能够找到一个更好的解决方案,不需要在每个请求中传递两个对我的context 实例的引用。

    我们只是创建一个接受嵌入式上下文和我们的处理程序类型的结构,并且由于ServeHTTP,我们仍然满足http.Handler 接口。处理程序不再是 appContext 类型上的方法,而是将其作为参数接受,这导致函数签名稍长,但仍然“明显”且易于阅读。如果我们担心“打字”,我们就会收支平衡,因为我们不再需要担心方法接收器。

    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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-01-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-10
      相关资源
      最近更新 更多