我是从 r59 —— 1.0 之前的一个发布版本,就开始写 Go 了,并且在过去七年里一直在用 Go 构建 HTTP API 和服务。
在 Machine Box 里,我大部分的技术性工作涉及到构建各种各样的 API。 机器学习本身很复杂而且大部分开发者也不会用到,所以我的工作就是通过 API 终端来简单阐述一下,目前来说反响很不错。
如果你还没有看过 Machine Box 开发者的经验, 请尝试一下 并让我知道你的意见。我编写服务的方法已经在过去几年中发生了变化,所以我打算分享目前我编写服务的经验——也许这些方法能帮到你和你的工作。
目录
一个 server 结构体
我所有的组件都有一个单独的 server 结构体,它通常都是类似于下面这种形式:
type server struct { db *someDatabase router *someRouter emailEmailSender }-公共组件是该结构体的字段。
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 errerror ) 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语言中文网 荣誉推出