将上下文传递给接口方法

huangapple go评论77阅读模式
英文:

Passing Context to Interface Methods

问题

受到上周这篇文章的启发,我正在考虑重构我的应用程序,以更明确地传递上下文(如数据库连接池、会话存储等)给我的处理程序。

然而,我遇到的一个问题是,在没有全局模板映射的情况下,我的自定义处理程序类型的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)- 如果我理解有误,请纠正我,因为我希望能更好地理解这个问题。

我并不完全相信在package main中使用全局变量是不好的(即不是库),只要它们在使用中是安全的(只读/互斥锁/池),但我确实喜欢显式传递上下文所提供的清晰性。

英文:

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.

答案1

得分: 6

我会使用闭包,像这样做:

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

然后只需使用返回的 http.Handler

你只需要确保你的 appContext 是协程安全的。

英文:

I would use a closure and do something like this:

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)
})
}

And just use the returned http.Handler.

You'll just have to make sure your appContext is goroutine-safe.

答案2

得分: 4

在与一些乐于助人的Gophers在#go-nuts上进行了一些讨论之后,我可以确定上述方法是“最好的选择”。

  • 这种方法的“缺点”是我们需要两次传递对上下文结构的引用:一次作为我们方法中的指针接收器,再一次作为结构成员,以便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 // 嵌入以便我们可以在处理程序中直接调用app.db或app.store。
}

// 在main()中...
context := &appContext{db: nil, store: nil}
r.Get("/", appHandler{context.IndexHandler, context}) 
...

最重要的是,这种方法完全兼容http.Handler,因此我们仍然可以像这样用通用中间件包装我们的处理程序结构:gzipHandler(appHandler{context.IndexHandler, context})

(但我仍然对其他建议持开放态度!)


更新

感谢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) {
    // 现在我们可以在这里访问我们的上下文。
    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()
    // 我们每个请求只传递一次对上下文的引用,看起来更简单
    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
}
英文:

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
}

huangapple
  • 本文由 发表于 2014年7月12日 23:51:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/24714624.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定