在使用Go七年后我如何编写Go HTTP服务

原文: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确保代码只执行一次,其他调用(其他人发出相同的请求)将一直阻塞,直到完成。

  1. 错误检查在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的迷你替代品)

结论

我希望本文中涉及的内容有意义,并帮助您完成工作。 如果您不同意或有其他想法,请发推特给我。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容