原文:https://medium.com/statuscode/how-i-write-go-http-services-after-seven-years-37c208122831
翻译:devabel
自从r59(一个1.0之前的版本)以来,我一直在写Go(那时还不叫Golang),并且在过去的七年里一直在Go中构建HTTP API和服务。
我在Machine Box工作,我的大多数技术工作都涉及构建各种API。 机器学习很复杂,大多数开发人员都无法掌握,因此我的工作是通过API接口简化这个过程,到目前为止我们已经得到了很好的反馈。
如果您还没有亲眼尝试过Machine Box开发者的体验,请试一试,让我知道您的想法。
多年来,我编写服务的方式发生了变化,因此今天我想分享如何编写服务 - 也许这种方式对您的工作有帮助。
A server struct
我的所有组件都有一个 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即可帮助我们调试。
处理程序挂起服务器
我的HTTP处理程序挂起了服务器:
func (s *server) handleSomething() http.HandlerFunc { ... }
处理程序可以通过s服务器变量访问依赖项。
返回处理程序
我的处理函数实际上并不处理请求,它们返回一个函数。
这给了我们一个闭包环境,我们的处理程序可以在其中运行
func (s *server) handleSomething() http.HandlerFunc {
thing := prepareThing()
return func(w http.ResponseWriter, r *http.Request) {
// use thing
}
}
prepareThing仅被调用一次,因此您可以使用它来执行一次性每个处理程序初始化,然后在处理程序中使用该事物。
确保只读取共享数据,如果处理程序正在修改任何内容,请记住您需要一个互斥锁或其他东西来保护它。
获取特定于处理程序的依赖项的参数
如果特定处理程序具有依赖项,请将其作为参数。
func (s *server) handleGreeting(format string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, format, "World")
}
}
格式变量可供处理程序访问。
处理程序上的HandlerFunc
我现在几乎在所有情况下都使用http.HandlerFunc,而不是http.Handler。
func (s *server) handleSomething() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
...
}
}
它们或多或少是可以互换的,所以只需选择更容易阅读的内容。 对我来说,这是http.HandlerFunc。
中间件只是一个Go函数
中间件函数接受一个http.HandlerFunc并返回一个可以在调用原始处理程序之前和/或之后运行代码的新函数 - 或者它可以决定根本不调用原始处理程序。
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)
}
}
处理程序内部的逻辑可以选择是否调用原始处理程序 - 在上面的示例中,如果IsAdmin为false,处理程序将返回HTTP 404 Not Found并返回(abort); 注意没有调用h处理程序。
如果IsAdmin为true,则执行将传递给传入的h处理程序。
通常我在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()))
}
请求和响应类型也可以在那里
如果一个接口有自己的请求和响应类型,通常它们仅对该特定处理程序有用。
如果是这种情况,您可以在函数内定义它们。
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) {
...
}
}
这会对您的包空间进行整理,并允许您将这些类型命名为相同,而不必考虑特定于处理程序的版本。
在测试代码中,您只需将类型复制到测试函数中并执行相同的操作即可。 要么…
测试类型可以帮助构建测试
如果您的请求/响应类型隐藏在处理程序中,您只需在测试代码中声明新类型即可。
这是一个为需要了解您的代码的后代做一些故事讲述的机会。
例如,假设我们的代码中有Person类型,我们在许多接口上重用它。 如果我们有一个/ greet接口,我们可能只关心他们的名字,所以我们可以在测试代码中表达:
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
从这个测试中可以清楚地看出,我们唯一关心的事情是人的名字。
sync.Once设置依赖项
如果我在准备处理程序时必须做任何昂贵的事情,我会推迟到第一次调用该处理程序时。
这提高了应用程序启动时间
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函数之外,所以如果出现问题我们仍然会出现错误并且不会在日志中丢失它
2.如果未调用处理程序,则永远不会完成昂贵的工作 - 这可能会带来很大的好处,具体取决于代码的部署方式
请记住,执行此操作时,您将初始化时间从启动时移至运行时(首次访问端点时)。 我经常使用Google App Engine,所以这对我来说很有意义,但是你的情况可能会有所不同,所以值得思考何时何地使用sync.Once这样。
服务器是可测试的
我们的服务器类型非常便于测试。
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, req)
is.Equal(w.StatusCode, http.StatusOK)
}
在每个测试中创建一个服务器实例 - 如果昂贵的东西延迟加载,这将不会花费太多时间,即使对于大组件
通过在服务器上调用ServeHTTP,我们正在测试整个堆栈,包括路由和中间件等。如果你想避免这种情况,你当然可以直接调用处理程序方法。
使用httptest.NewRecorder记录处理程序正在执行的操作
此代码示例使用我的测试迷你框架(作为Testify的迷你替代品)
结论
我希望本文中涉及的内容有意义,并帮助您完成工作。 如果您不同意或有其他想法,请发推特给我。