GopherCon 2019 - 八年之后我如何写http web服务(翻译)

Kenigbolo Meya Stephen 写于2019年7月26日


宣讲人: Mat Ryer

作者博客: Kenigbolo Meya Stephen

概述

看看Mat Ryer在过去八年中是如何构建web服务的,非常实用,可靠、有效的模式,每个人可受用


关于Mat Ryer

Mat是go语言先行者,甚至在go的v1版本前就开始使用了。最近工作在 Machine Box and Veritone。他也是开源软件贡献者,在BitBar, Testify, Gopherize.me开源软件上都有他的身影。Mat使用go语构建http服务有很多年了,在此过程中学习到了很多,也贡献很多。你可以在twitter上找他@matryer

编写HTTP服务要考虑的重点

Mat 在关于写http服务上要考虑的因素上突出部分要点。他重申这些点是非常重要的,可以帮助你写的http服务清晰、简明,这些要点如下:

  • 可维护性

将可维护性考虑到任何你要写的服务中是非常重要的。如果在初期架构阶段不考虑可维护性,后期成本可能会很高

  • 易读性

当浏览代码时,你多快能看懂?能多长时间就把代码库都浏览一遍?这也可以当成是编写代码的一种准则,没有任何东西是很复杂的难以找到的。这包括函数命名,包命名,变量命名,代码结构,工程结构等等

  • 代码不要花哨

代码不要花哨指的是代码库应该是浅显易懂的。不是要你写的有多"风骚"而是要你写的别人都懂。秘记于心:这些代码可能会被没有经验的开发者使用

  • 代码风格要保持统一

代码风格保持统一有助于提高其它开发人员对代码库的熟悉程度

设计模式

在考虑了上面列出的不同因素之后,下一步是基于这些因素做出设计决策。虽然存在不同的用例,重要的是使用自己最合适的,Mat相信下面列出的设计模式用例对于编写http服务非常有用

创建一个最小main抽象

func main () {
  if err := run(); err != nil {
      fmt.Fprintf(os.Stderr, “%s\n”, err)
      os.Exit(1)
  }
}

func run() error {
  db, dbtidy, err := setupDatabase()
  if err != nil {
      return errors.Wrap(err, “setup database”)
  }
  defer dbtidy()
  srv := &server{
      db: db,
  }
  //... more stuff

Mat认为像上面的这个小的抽象允许它返回一个error,而不是要在main中处理多个error。这样run函数对http服务启动负责就行了。run函数要做的也就是准备好服务,调用ListenAndServe

创建一个server结构体

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

db,router,email等变量要放在结构体server中,而不是放在包空间中。要尽量避免全局状态,server结构体能够帮你做到。这个也可以帮你清楚的知道server依赖项

创建server构造函数

func newServer() *server {
s := &server{}
s.routes()
return s
}

注意不要在构造器里赋值依赖项。如果你需要赋值依赖项,在结构体上做,意思是,如果想给server中的依赖赋值,这样做:

  s:=newServer()
  s.db = XXX
  s.email = XXX

给server加个http.Handle函数

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}

这里的目的就是将执行函数传给router.理想情况下这里不要有逻辑.如果你想放一些逻辑在这里,考虑把这些逻辑移到中间件去

默认路由文件

package main

func (s *server) routes() {
  s.router.Get("/api/", s.handleAPI())
  s.router.Get("/about", s.handleAbout())
  s.router.Get(“/", s.handleIndex())
}

单独把路由服务放一个文件里是可取的,这种做法很优雅,在一处能看到所有的路由服务信息

Handlers弄挂了服务器

func (s *server) handleSomething() http.HandlerFunc { 
  // put some programming here
}

每一个http请求都会启动一个goroutine,所以要小心。因为handlers是server的函数,他们总会访问server变量,所以必须了解资源竞争情况(其它handler可能同时访问server变量)

对Handler方法命名

handleTasksCreate
handleTasksDone
handleTasksGet

handleAuthLogin
handleAuthLogout

建议根据职责对名称进行分组。这样更容易寻找相关函数同时也提高了可读性。相关联的函数功能组理想情况下也应放一起

返回 handler

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

这个handler给你一个闭包环境,如果你有特殊处理可以放在这里

带参数的特殊handler

func (s *server) handleGreeting(format string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, format, r.FormValue(“name”))
}
}
s.router.HandleFunc(“/one”, s.handleGreeting(“Hello %s”))
s.router.HandleFunc(“/two”, s.handleGreeting(“Hola %s”))

如果有特殊依赖项,你不希望放在server类型中,你可以把它当参数放在handler方法里。这样访问这个依赖赖就限制在这个handler中了。这使得很容易看到handler究竟需要做什么工作。而且,就类型安全性来讲,这是非常好的,因为你不提供依赖项,你就获得不了handler
Y

handler和handler func比较

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

  }
}

使用handler函数的主要目的是帮助处理类型。如果你想创建一个handler,你不必要要创建这个handler类型,因为这个匿名函数已经将它转化成http handler类型了(如上代码所示)
PS-如果你发现你经常在handlers和handler func切换,也许你更合适坚持用handlers(这部分翻译可能不达意)

中间件就是go函数

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)
}
}

中间件仅仅是普通的go函数,这个go函数将handler作为参数,并返回一个新的handler,中间件可以做好多的事。它可以在原handler调用前或在处理后执行相关操作。某些情况下,原handler可能都不会被调用。你可以使用中间件做日志,追踪和鉴权等等操作

在路由文件中包裹上你的中间件

package main
func (s *server) routes() {
s.router.Get("/api/", s.handleAPI())
s.router.Get("/about", s.handleAbout())
s.router.Get("/", s.handleIndex())
s.router.Get(“/admin", s.adminOnly(s.handleAdminIndex()))
}

这样可以使你的路由文件“routes.go"有更高级的映射,这里也可以看到你所有API在服务中的功能足迹

处理数据

作为开发者,我们总是禁不住抽象功能,保持代码的精简。但是,很多情况下,我们抽象的太早了,可能是因为我们对于"抽干"代码的执着,这是应该抵制的。处理数据需要一些抽象,我们来看看过去8年时,那些尝试过的、经验定过的的方法:

响应辅助函数

func (s *server) respond(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
w.WriteHeader(status)
if data != nil {
  err := json.NewEncoder(w).Encode(data)
  // TODO: handle err
}
}

这种抽象的巨大好处是,对于http服务的响应,当需要发生改动时,改动这个函数就行了。也就是说,它给了你更多的灵活性和更小的代码重复。响应辅助函数初期通常简单,短小

解析辅助函数

func (s *server) decode(w http.ResponseWriter, r *http.Request, v interface{}) error {
return json.NewDecoder(r.Body).Decode(v)
}

就像响应helper一样,它使你抽象出解析功能。它给了你改变这一个地方就能影响你整个http服务的灵活性

具有前瞻性的辅助函数

你可以前瞻性的设计任何helper,记得带上response writer和request一起作为输入参数.即使你在初期用不到它们,通常情况在go语言中处理http你也只需要与这两个参数打交道.(意思是,如上decode函数所示,用不到http.ResponseWriter,但还是把它作为了一个输入参数,目的是为将来扩展考虑,因为go语言http响应也只要处理这两个变量,带上也无防)

请求和响应数据类型

func (s *server) handleGreet() http.HandlerFunc {
type request struct {
  Name string
}
type response struct {
  Greeting string `json:"greeting"`
}
return func(w http.ResponseWriter, r *http.Request) {
  ...
}
}

如果一个终端拥有自己的请求和响应类型,通常情况下他们仅仅在特定的handler中有用。如果是这种情况的话,你可以在函数内部定义他们。这使你的包空间清洁,同时,你可以把这些类型命名一样,而不用想着每个handler的数据类型都要特别命名(意思是,如果将请求和返回类型定义在包空间里而不是handler函数里,你可能要命名无数多个XXXRequest和XXXResponse,起名字很纠结啊...)

使用sync.Once延迟初始化

func (s *server) handleTemplate(files string...) http.HandlerFunc {
var (
  init    sync.Once
  tpl     *template.Template
  tplerr  error
)
return func(w http.ResponseWriter, r *http.Request) {
  init.Do(func(){
    tpl, tplerr = template.ParseFiles(files...)
  })
  if tplerr != nil {
    http.Error(w, tplerr.Error(), http.StatusInternalServerError)
    return
  }
  // use tpl
}
}

sync.One给你的好处是,当handler被调用时才运行这部分代码,而不用启动时就运行。启动太沉重会减慢服务启动时间,因此当调用时再运行这段代码,能极大的改善这种情况

Testing 测试

测试是软件易维护性的好工具,无论使用什么语言编写的http服务,测试都需要。我确定你需要!
httptest包应该是你的好助手,在默认情况下就要用这个包去测试http服务,因为这真的很有用。你可以用它创建http request请求,它和http.NewRequest不一样,它不会返回error,因此使编写更容易。模拟传入http请求的能力非常有用

Summary 总结

Matt Ryer的这篇演讲基于他写的一篇博客文章。这篇文章被广泛传播,在go社区很有名,查看请点击 这里。 这篇文章也在reddit上分享后,引起很多讨论,反馈和提议。这次演讲是自他从上次来学习后的巅峰,它的重点是他的思想背后的哲学,他喜欢的方法,而不是对一些具体的规则集的强硬路线。
他强调技术leader,管理人员,CTO等等,应该努力创建一个允许工程师“尝新”机会的氛围,只要在合理范畴内。这次演讲内容也不是要大家盲目遵从,毕竟不同的团队有不同的需求和使用用例。

你可能感兴趣的:(GopherCon 2019 - 八年之后我如何写http web服务(翻译))