自从go语言r59版本(一个1.0以前的版本)以来,我一直在写Go程序,而且在过去七年里一直在Go中构建HTTP API和服务.json
多年来,我编写服务的方式发生了变化,因此我想分享今天如何编写服务 - 以防模式对您和您的工做有用.api
个人全部组件都有一个server
结构,一般看起来像这样:服务器
type server struct { db * someDatabase router * someRouter email EmailSender }
共享依赖项是结构的字段闭包
我在每一个组件中都有一个文件routes.go
,其中全部路由均可以存在:app
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
server 挂载 handler
:函数
func(s * server)handleSomething()http.HandlerFunc {...}
handler能够经过s服务器变量访问依赖项.测试
个人处理函数实际上并不处理Request,它们返回一个handler函数.spa
这给了咱们一个闭包环境
,咱们的处理程序能够在其中运行3d
func(s * server)handleSomething()http.HandlerFunc { thing:= prepareThing() return func(w http.ResponseWriter,r * http.Request){ // use thing } }
该prepareThing
只调用一次,因此你能够用它作一次每处理程序初始化,而后用thing在处理程序.
确保只读取共享数据,若是处理程序正在修改任何内容,请记住您须要一个互斥锁或其余东西来保护它.
若是特定处理程序具备依赖项,请将其做为参数.
func(s * server)handleGreeting(format string)http.HandlerFunc { return func(w http.ResponseWriter,r * http.Request){ fmt.Fprintf(w,format,"World") } }
format处理程序能够访问该变量.
Handler
我http.HandlerFunc
如今几乎用在每个案例中,而不是http.Handler
.
func(s * server)handleSomething()http.HandlerFunc { return func(w http.ResponseWriter,r * http.Request){ ... } }
它们或多或少是能够互换的,因此只需选择更容易阅读的内容.对我来讲,就是这样http.HandlerFunc.
中间件函数接受http.HandlerFunc并返回一个能够在调用原始处理程序以前和/
或以后运行代码的新函数 - 或者它能够决定根本不调用原始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) } }
处理程序内部的逻辑能够选择是否调用原始处理程序 - 在上面的示例中,若是IsAdmin
是false
,HandlerFunc将返回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())) }
若是Server有本身的请求
和响应
类型,一般它们仅对该特定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){ . .. } }
这会对您的包空间
进行整理,并容许您将这些类型命名为相同
,而没必要考虑特定于处理程序的版本.
在测试代码中,您只需将类型复制到测试函数中并执行相同的操做便可.要么…
若是您的请求/响应
类型隐藏在处理程序中,您只需在测试代码中声明新类型便可.
这是一个为须要了解您的代码的后代作一些故事讲述的机会.
例如,假设Person咱们的代码中有一个类型,咱们在许多端点上重用它.若是咱们有一个/greet
endpoint,咱们可能只关心他们的名字,因此咱们能够在测试代码中表达:
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) / / ...这里有更多测试代码
从这个测试中能够清楚地看出,咱们关心的惟一领域就是Name人.
若是我在准备处理程序时必须作任何昂贵的事情,我会推迟到第一次调用该处理程序时.
这改善了应用程序启动时间
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 } }
其余调用(其余人发出相同的请求)将一直阻塞,直到完成.
错误检查在init函数以外,因此若是出现问题咱们仍然会出现错误,而且不会在日志中丢失错误
若是未调用处理程序,则永远不会完成昂贵的工做 - 根据代码的部署方式,这可能会带来很大的好处
请记住,执行此操做时,您将初始化时间从启动时移至运行时(首次访问端点时).我常用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(错误) w:= httptest.NewRecorder() srv.ServeHTTP(w,req) is.Equal(w.StatusCode,http.StatusOK) }
在每一个测试中建立一个服务器实例 - 若是昂贵的东西延迟加载,这将不会花费太多时间,即便对于大组件
经过在服务器上调用ServeHTTP
,咱们正在测试整个堆栈,包括路由和中间件等.若是你想避免这种状况,你固然能够直接调用处理程序方法.
使用httptest.NewRecorder
记录什么处理程序在作
此代码示例使用个人测试迷你框架(做为Testify
的迷你替代品)