GopherCon 2019 - 8 年过去了,我是如何写 Go Web Server 的?

Go

主持人: Mat Ryer
直播博主: Kenigbolo Meya Stephen

概述

看看 Mat Ryer 在过去八年中是如何构建 web 服务的。是每个人现在都可以开始使用的非常实用且经过测试的模式。


关于 Mat Ryer

Mat 是 Go 的先行者。甚至在 Go 推出第一个主要版本(v1)之前,他就开始使用 Go 了。他目前在 Machine Box 和 Veritone 工作。他也是开源的狂热者,在 Bitbar、Gopherize.me、Gopherize.me 等开源项目中都可以找到他的身影。他在 Go 中构建 https 服务已经很长时间了,在这一过程中学到了很多东西,也改变了很多东西。你可以在推特上找到他,地址是@matryer。

编写HTTP服务时需要考虑的因素

Mat重点介绍了编写HTTP服务时需要考虑的一些非常重要的因素。他重申,这些因素很重要,有助于您编写清晰简洁的https服务。这些因素包括

  • 可维护性

重要的是要考虑要编写的任何服务的可维护性。如果在创建阶段未考虑维护成本,则维护成本可能会大于创建工具的初始成本。

  • 舒适性

当你阅览代码时,理解的速度有多快?在代码库中导航的速度有多快?这也可以看作是以这样一种方式编写代码,任何东西都不会很复杂地找到。这包括函数、命名空间、变量名、代码结构、项目结构等的命名。

  • 代码是无趣的

这里的代码应该是无趣的,意思是关于代码库的一切都是显而易见的。这不是写花哨的东西,而是写给其他人理解的东西。重要的是,我们了解该代码可能会由经验不足或没有经验的人使用。

  • 自相似代码

编写与代码库中其他代码相似的代码,有助于提高必须处理代码的人员的熟悉度。

设计模式/决策

在考虑了上面列出的不同因素之后,下一步就是根据这些考虑做出设计决策/模式。尽管存在不同的用例,重要的是人们使用最适合自己的用例,但 Mat 认为,在大多数情况下,下面列出的设计模式对于编写 https 服务实际上非常有用。

创建一个微小的抽象 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 认为,像上面的这样的微小抽象使得他可以返回错误,而不是对特定代码进行错误处理。这允许运行功能实际负责运行 https 服务,即只需设置服务并调用 ListenAndServe

创建一个服务器结构

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

而不是将以上代码放在包空间中,而是将其放在服务器中(代码结构)。避免全局状态是件好事,拥有服务器结构有助于避免这种情况。这使得服务器的需求显而易见。

为服务器创建构造函数

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

重要的是不要在服务器构造函数中设置依赖关系。如果您需要设置依赖关系,则可以使用服务器结构。

让 server 成为 http.Handle

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

这里的目标是将执行传递给路由器。理想情况下,您永远不要在这里放逻辑。如果您希望在此处放置任何逻辑,请考虑将其移入中间件文件。

默认路由文件

package main

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

一个包含所有路由的文件总是有用的,因为它很容易浏览。它的优点是可以在一处查看 http 服务的所有路由。

Handlers 挂在 server 上

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

每一个 HTTP 请求都对应着一个 goroutine , 其他 goroutine 也可以访问 s,因此必须注意数据争用之类的情况。

命名 Handler 方法

handleTasksCreate
handleTasksDone
handleTasksGet

handleAuthLogin
handleAuthLogout

建议根据职责对名称进行分组。这样更容易阅读和查找。理想情况下,相关功能应始终分组在一起。

返回 handler

func (s *server) handleSomething() http.HandlerFunc {
  thing := prepareThing()
  return func(w http.ResponseWriter, r *http.Request) {
      // 可以使用变量 thing   
  }
}

handler 返回一个闭包,你可以在其中设置 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”))

如果您具有特定的依赖关系,而又不想保留服务器类型,则可以将其作为此处理程序方法的参数。允许您执行的操作是在小处理程序函数中访问它们。这使得查看处理程序完成其工作所需的内容变得非常容易。此外,就类型安全而言,这也非常有用,因为如果不提供其依赖项,则无法获取处理程序。

Y

处理函数过度处理

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

  }
}

使用处理程序功能的主要目标是帮助类型。如果要创建处理程序,则无需使用处理程序函数来创建类型,因为这样做可以选择使用匿名函数并将其强制转换为HTTP处理程序函数,如上面的代码所示。

PS-如果您发现自己经常在处理程序和处理程序功能之间切换,那么最好还是坚持使用处理程序。

中间件只是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函数,它将处理程序作为参数并返回一个可以执行不同操作的新处理程序。它可以在调用原始处理程序之前或之后调用原始处理程序。在某些情况下,根本不需要调用原始处理程序。您可以在日志记录,跟踪,身份验证等中使用中间件

将您的中间件软件连接到routes.go

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文件成为http服务的高级映射。这也很有用,因为您只有一个地方,可以容纳需要解释HTTP服务API足迹的所有内容。

处理数据

作为开发人员,我们总是很想抽象功能并尽可能使代码保持DRY。但是,很多时候我们过早地进行抽象,这主要是由于我们对DRY代码的痴迷,但这是值得抗拒的。但是,处理数据需要一些抽象,我们将寻找那些经过八年尝试和测试的抽象。

响应助手

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

就像响应助手一样,这使您可以抽象解码功能。这使您可以灵活地在一个位置进行更改,从而影响整个http服务。

Future proof helpers

您可以使用始终接受响应编写者和请求的简单规则,将来证明您编写的任何帮助程序。即使您在一开始就不需要它们,通常这也是您真正需要处理go的所有内容

请求和响应数据类型

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

如果端点具有自己的请求和响应类型,通常它们仅对特定处理程序有用。如果是这样,您可以在函数中定义它们。这使您的程序包空间更加整洁,并允许您为这些类型命名相同的名称,而不必考虑处理程序特定的版本。

尽管将请求和响应类型放在包空间中是很常见的,但是将它们放在稍微封闭的环境中(如上面的代码片段所示)可以帮助您进行整理。

惰性设置同步一次

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

同步程序使您能够在首次调用给定处理程序时(而不是在程序首次启动时)运行代码。昂贵的设置会减慢服务启动时间,因此只有在首次调用时才运行它,才能大大改善服务启动时间。

测试

测试是可维护性的一个很好的工具,这是http服务中所需要的,无论它们使用哪种语言编写。您确实需要测试。

httptest软件包应该是您最好的朋友,并且您默认使用go软件包来测试http服务,因为它非常有用。但是,您可以使用它创建HTTP请求,与可以返回错误的HTTP.NewRequest函数不同,httptest不会,因此使编写测试变得更加容易。模拟进入的HTTP请求的功能超级有用

摘要

Matt Ryer的演讲基于他撰写的博客文章。可以在这里找到的博客文章具有病毒性。它在围棋社区中非常流行,绝对值得一读。该帖子在reddit上分享,并引发了很多问题,反馈和建议。这次演讲是他自那时以来所学到的最高成果。它专注于他偏爱的想法背后的哲学,而不是针对某些特定规则集的强硬路线。

他强调,技术主管,工程经理,CTO等应努力创建一个缓冲区,允许工程师在合理范围内尝试新事物。由于不同的团队可能有不同的需求和用例/边际案例,因此该演讲不是盲目遵循的。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://about.sourcegraph.com/go/gopherc...

译文地址:https://learnku.com/go/t/48277

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!