Go语言HTTP服务最佳实践(译)

图片描述

自从go语言r59版本(一个1.0之前的版本)以来,我一直在写Go程序,并且在过去七年里一直在Go中构建HTTP API和服务.

多年来,我编写服务的方式发生了变化,所以我想分享今天如何编写服务 - 以防模式对您和您的工作有用.

1. Server Struct

我的所有组件都有一个server结构,通常看起来像这样:

type server struct { 
    db * someDatabase 
    router * someRouter 
    email EmailSender 
}

共享依赖项是结构的字段

2. 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可以指示我们在哪里查看.

3. server 挂载 handler

我的HTTPserver 挂载 handler

func(s * server)handleSomething()http.HandlerFunc {...}
handler可以通过s服务器变量访问依赖项.

4. return Handler

我的处理函数实际上并不处理Request,它们返回一个handler函数.

这给了我们一个闭包环境,我们的处理程序可以在其中运行

func(s * server)handleSomething()http.HandlerFunc { 
    thing:= prepareThing()
    return func(w http.ResponseWriter,r * http.Request){ 
        // use thing         
    } 
}

prepareThing只调用一次,所以你可以用它做一次每处理程序初始化,然后用thing在处理程序.

确保只读取共享数据,如果处理程序正在修改任何内容,请记住您需要一个互斥锁或其他东西来保护它.

5. 参数是handler函数的依赖

如果特定处理程序具有依赖项,请将其作为参数.


func(s * server)handleGreeting(format string)http.HandlerFunc { 
    return func(w http.ResponseWriter,r * http.Request){ 
        fmt.Fprintf(w,format,"World")
    } 
}

format处理程序可以访问该变量.

6. HandlerFunc over Handler

http.HandlerFunc现在几乎用在每一个案例中,而不是http.Handler.

func(s * server)handleSomething()http.HandlerFunc { 
    return func(w http.ResponseWriter,r * http.Request){ 
        ... 
    } 
}

它们或多或少是可以互换的,所以只需选择更容易阅读的内容.对我来说,就是这样http.HandlerFunc.

5. Middleware中间件

中间件函数接受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)
    } 
}

处理程序内部的逻辑可以选择是否调用原始处理程序 - 在上面的示例中,如果IsAdminfalse,HandlerFunc将返回HTTP 404 Not Found并返回(abort); 注意没有调用h处理程序.

如果IsAdmintrue,则将执行传递给传入的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()))
}

7. Request 和 Response类

如果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){ 
        . .. 
    } 
}

这会对您的包空间进行整理,并允许您将这些类型命名为相同,而不必考虑特定于处理程序的版本.

在测试代​​码中,您只需将类型复制到测试函数中并执行相同的操作即可.要么…

8. 测试框架

如果您的请求/响应类型隐藏在处理程序中,您只需在测试代码中声明新类型即可.

这是一个为需要了解您的代码的后代做一些故事讲述的机会.

例如,假设Person我们的代码中有一个类型,我们在许多端点上重用它.如果我们有一个/greetendpoint,我们可能只关心他们的名字,所以我们可以在测试代码中表达:

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人.

9. 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 
    } 
}

10. sync.Once 确保代码只执行一次

其他调用(其他人发出相同的请求)将一直阻塞,直到完成.
错误检查在init函数之外,所以如果出现问题我们仍然会出现错误,并且不会在日志中丢失错误
如果未调用处理程序,则永远不会完成昂贵的工作 - 根据代码的部署方式,这可能会带来很大的好处
请记住,执行此操作时,您将初始化时间从启动时移至运行时(首次访问端点时).我经常使用Google App Engine,所以这对我来说很有意义,但是你的情况可能会有所不同,所以值得思考何时何地使用sync.Once这样.

11. 服务器是可测试的

我们的服务器类型非常可测试.

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的迷你替代品)

原文地址

(译)Go语言HTTP服务最佳实践-tech.mojotv


golang后端
Go语言后端技术分享,希望 [链接]的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交...

mojotv.cn

447 声望
33 粉丝
0 条评论
推荐阅读
Go语言详解:HTTP断点续传多线程下载原理
原文地址:Go语言详解:HTTP断点续传多线程下载原理

mojotv_cn阅读 2.1k

再也不学AJAX了!(三)跨域获取资源 ① - 同源策略
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第四篇,最近更新于 2023 年 1...

libinfs19阅读 4.1k评论 4

封面图
一个HTTP请求的曲折经历
作为程序员的我们每天都在和网络请求打交道,而前端程序员接触的最多的就是HTTP请求。平时工作中,处理网络请求之类的操作是最多的了。但是一个请求从客户端发出到被服务端处理、再回送响应,再被客户端接收这一...

nero24阅读 5.1k评论 1

前端如何入门 Go 语言
类比法是一种学习方法,它是通过将新知识与已知知识进行比较,从而加深对新知识的理解。在学习 Go 语言的过程中,我发现,通过类比已有的前端知识,可以更好地理解 Go 语言的特性。

robin23阅读 3.3k评论 6

封面图
前端优化之 Http 相关优化总结
学习和总结文章同步发布于 [链接],有兴趣可以关注一下,一起学习和进步。Http 优化方式是前端性能优化的重要部分,也是前端必备的知识点之一。减少静态资源文件大小这个是最根本的途径,假设真的有个 10 几兆以...

Samon17阅读 3.3k

Golang 中 []byte 与 string 转换
string 类型和 []byte 类型是我们编程时最常使用到的数据结构。本文将探讨两者之间的转换方式,通过分析它们之间的内在联系来拨开迷雾。

机器铃砍菜刀24阅读 58.2k评论 2

浏览器之HTTP缓存的那些事
简单来说,浏览器缓存就是把一个已经请求过的Web资源(如html,图片,js)拷贝一份副本储存在浏览器中。缓存会根据进来的请求保存输出内容的副本。当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制...

Samon16阅读 9.9k评论 7

mojotv.cn

447 声望
33 粉丝
宣传栏