我这几年来是如何编写 Go HTTP 服务的(转载)

一个 server 结构体

我所有的组件都有一个单独的 server 结构体,它通常都是类似于下面这种形式:

type server struct {
    db     *someDatabase
    router *someRouter
    email  EmailSender
}

-公共组件是该结构体的字段。

routes.go

在每个组件中我有个一个唯一的文件 routes.go ,在这里所有的路由都能运行。

package app
func (s *server) routes() {
    s.router.HandleFunc("/api/", s.handleAPI())
    s.router.HandleFunc("/about", s.handleAbout())
    s.router.HandleFunc("/", s.handleIndex())
}

这样会很方便,因为大部分代码维护都开始于一个 URL 和一个被报告的错误——所以只要浏览一下 routes.go 就能引导我们到目的地。

挂起服务器的 handler

我的 HTTP handler 挂起服务器:

func (s *server) handleSomething() http.HandlerFunc { ... }

handler 可以通过 s 这个server变量来访问依赖项。

返回 handler

我的 handler 函数不会处理请求,它们返回的函数完成处理工作。

这样会提供给我们一个 handler 可以运行的封闭环境。

func (s *server) handleSomething() http.HandlerFunc {
    thing := prepareThing()
    return func(w http.ResponseWriter, r *http.Request) {
        // use thing
    }
}

prepareThing 函数只会被调用一次,所以你可以用它来完成每个 handler 的一次性的初始化工作,然后在 handler 里面使用 thing 。

请确保只会对共享数据执行读操作,如果 handler 改写了共享数据,记住你需要用锁或者其他机制来保护共享数据。

为 handler 专有的依赖传递参数

如果一个特别的 handler 有一个依赖项,就把这个依赖项当作参数。

func (s *server) handleGreeting(format string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, format, "World")
    }
}

format 变量可以被 handler 访问。

用 HandlerFunc 代替 Handler

现在我几乎在每个用例中都会使用 http.HandlerFunc ,而不是 http.Handler 。

func (s *server) handleSomething() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ...
    }
}

两者之间大都可以互换,所以觉得哪个便于阅读就选哪个即可。对我来说,http.HandlerFunc 更加适合。

中间件仅仅只是 Go 函数

中间件函数接受一个 http.HandlerFunc 并且返回一个新的 HandlerFunc , 该 handler 可以在调用初始 handler 之前或之后运行代码——抑或它可以决定是否调用初始的 handler 。

func (s *server) adminOnly(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !currentUser(r).IsAdmin {
            http.NotFound(w, r)
            return
        }
        h(w, r)
    }
}

这个 handler 内部的逻辑可以选择性的决定是否调用初始 handler——在上面的例子里,如果 IsAdmin 为 false,该 handler 就会返回一个 HTTP 404 Not Found 并且返回(中止);注意,h handler 没有被调用。

如果 IsAdmin 为 true, 就会运行到 h handler。

通常我会把中间件放到 routes.go 文件中:

package app
func (s *server) routes() {
    s.router.HandleFunc("/api/", s.handleAPI())
    s.router.HandleFunc("/about", s.handleAbout())
    s.router.HandleFunc("/", s.handleIndex())
    s.router.HandleFunc("/admin", s.adminOnly(s.handleAdminIndex))
}

request 和 response 类型也可以放在那里

如果终端有它自身的 request 和 response 类型的话,通常这些类型只对特定的 handler 有用。

假设一个例子,你可以把它们定义在函数内部。

func (s *server) handleSomething() http.HandlerFunc {
    type request struct {
        Name string
    }
    type response struct {
        Greeting string `json:"greeting"`
    }
    return func(w http.ResponseWriter, r *http.Request) {
        ...
    }
}

这样就可以解放包的空间,并允许你把这种类型都定义成同样的名字,从而免去了特定的 handler 考虑命名。

在测试代码时,你可以直接复制这些类型到你的测试函数中并执行同样的操作。或者其他……

测试类型有助于架构测试的框架

如果你的 request/response 类型都隐藏在 handler 内部,那么你可以在测试代码中直接定义新的类型。

这就有机会做一些解释性的工作,以便让未来的接任者能够理解你的代码。

举个例子,我们假设代码中一个 Person 类型存在,并且在很多终端都会重用它。如果我们有一个 /greet 终端,这时可能只关心它的 Name,所以可以在测试代码中这样表述:

func TestGreet(t *testing.T) {
    is := is.New(t)
    p := struct {
        Name string `json:"name"`
    }{
        Name: "Mat Ryer",
    }
    var buf bytes.Buffer
    err := json.NewEncoder(&buf).Encode(p)
    is.NoErr(err) // json.NewEncoder
    req, err := http.NewRequest(http.MethodPost, "/greet", &buf)
    is.NoErr(err)
    //... more test code here

从测试代码中可以清晰的看出,我们只关心 Person 的 Name 字段。

sync.Once 组织依赖

如果我不得不为准备 handler 时执行一些代价高昂的操作,我就会把它们推迟到第一次调用 handler 的时刻。

这样可以改善应用的启动时间。

func (s *server) handleTemplate(files string...) http.HandlerFunc {
    var (
        init sync.Once
        tpl  *template.Template
        err  error
    )
    return func(w http.ResponseWriter, r *http.Request) {
        init.Do(func(){
            tpl, err = template.ParseFiles(files...)
        })
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        // use tpl
    }
}

sync.Once 确保了代码只会运行一次,如果有其它调用(其他人发起同样的请求)就会堵塞,直到代码结束为止。

  • 错误检查放在了 init 函数外面,所以如果出现错误的话我们依然可以捕获它,而且不会在日志中遗失 。
  • 如果 handler 没有被调用过,这些代价高昂的操作就永远不会发生——这可以对你的代码部署有极大好处。

记住这一点,上面是把初始化的时间从启动时刻移到了运行时刻(当端点第一次被访问到时)。我使用 Google App Engine 很久了,对我来说这种操作是可以理解的,但对你自身来说可能就未必了。所以你有必要思考何时何地值得用 sync.Once 这种方式。

server 必须易于测试

我们的 server 类型需要能够简单测试。

func TestHandleAbout(t *testing.T) {
    is := is.New(t)
    srv := server{
        db:    mockDatabase,
        email: mockEmailSender,
    }
    srv.routes()
    req, err := http.NewRequest("GET", "/about", nil)
    is.NoErr(err)
    w := httptest.NewRecorder()
    srv.ServeHTTP(w, r)
    is.Equal(w.StatusCode, http.StatusOK)
}
  • 在每组测试中创建一个 server 实例——如果把代价高昂的操作延迟加载,这就不会花费太多时间,即使是对大型组件也依然有效。
  • 通过调用服务器上的 ServerHTTP ,我们会测试到整个栈,包括路由和中间件等。当然了,如果希望避免这种情况的话,你也可以直接调用 handler 函数。
  • 使用 httptest.NewRecorder 来记录 handler 所执行的操作。
  • 这份代码示例使用到了我的 一个正在测试中的微框架 (一个验证用的简易可选项)

结论

我希望文章中涵盖到的内容可能对你有些用处,能帮助到你的工作。如果你有不同意见或其它想法的话, 请联系我们 。


via: 我这几年来是如何编写 Go HTTP 服务的

作者:Mat Ryer 译者:sunzhaohao 校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出

猜你喜欢

转载自blog.csdn.net/lengyue1084/article/details/108246747