内容简介:原文:翻译:devabel自从r59(一个1.0之前的版本)以来,我一直在写Go(那时还不叫Golang),并且在过去的七年里一直在Go中构建HTTP API和服务。
原文: 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
我的所有组件都有一个服务器结构,通常看起来像这样:
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的迷你替代品)
结论
我希望本文中涉及的内容有意义,并帮助您完成工作。 如果您不同意或有其他想法,请发推特给我。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用 Clojure 编写 OpenWhisk 操作,第 1 部分: 使用 Lisp 方言为 OpenWhisk 编写简明的代码
- 使用Sphinx编写文档
- 使用python编写游戏修改器
- 使用 Go 编写 PostgreSQL 触发器
- 使用 Rust + WebAssembly 编写 crc32
- 使用Fake和Paket编写F#脚本
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。