5.1 web 开发简介

因为Go的 net/http包提供了基础的路由函数组合与丰富的功能函数。所以在社区里流行一种用Go编写api不需要框架的观点;在我们看来,如果你的项目的路由在个位数、URI 固定且不通过 URI 来传递参数,那么确实使用官方库也就足够。但在复杂场景下,官方的 http 库还是有些力有不逮。例如下面这样的路由:

  1. GET /card/:id
  2. POST /card/:id
  3. DELTE /card/:id
  4. GET /card/:id/name
  5. ...
  6. GET /card/:id/relations

可见是否使用框架还是要具体问题具体分析的。

Go的Web框架大致可以分为这么两类:

  1. Router框架
  2. MVC类框架

在框架的选择上,大多数情况下都是依照个人的喜好和公司的技术栈。例如公司有很多技术人员是PHP出身,那么他们一定会非常喜欢像beego 这样的框架,但如果公司有很多 C 程序员,那么他们的想法可能是越简单越好。比如很多大厂的 C 程序员甚至可能都会去用 C 去写很小的 CGI 程序,他们可能本身并没有什么意愿去学习MVC或者更复杂的 web 框架,他们需要的只是一个非常简单的路由(甚至连路由都不需要,只需要一个基础的HTTP协议处理库来帮他省掉没什么意思的体力劳动)。

Go的 net/http 包提供的就是这样的基础功能,写一个 http echo server 只需要30s。

  1. //brief_intro/echo.go
  2. package main
  3. import (...)
  4. func echo(wr http.ResponseWriter, r *http.Request) {
  5. msg, err := ioutil.ReadAll(r.Body)
  6. if err != nil {
  7. wr.Write([]byte("echo error"))
  8. return
  9. }
  10. writeLen, err := wr.Write(msg)
  11. if err != nil || writeLen != len(msg) {
  12. log.Println(err, "write len:", writeLen)
  13. }
  14. }
  15. func main() {
  16. http.HandleFunc("/", echo)
  17. err := http.ListenAndServe(":8080", nil)
  18. if err != nil {
  19. log.Fatal(err)
  20. }
  21. }

如果你过了30s还没有完成这个程序,请检查一下你自己的打字速度是不是慢了(开个玩笑 :D)。这个例子是为了说明在Go中写一个HTTP协议的小程序有多么简单。如果你面临的情况比较复杂,例如几十个接口的企业级应用,直接用 net/http 库就显得不太合适了。

我们来看看开源社区中一个 kafka 监控项目中的做法:

  1. //Burrow: http_server.go
  2. func NewHttpServer(app *ApplicationContext) (*HttpServer, error) {
  3. ...
  4. server.mux.HandleFunc("/", handleDefault)
  5. server.mux.HandleFunc("/burrow/admin", handleAdmin)
  6. server.mux.Handle("/v2/kafka", appHandler{server.app, handleClusterList})
  7. server.mux.Handle("/v2/kafka/", appHandler{server.app, handleKafka})
  8. server.mux.Handle("/v2/zookeeper", appHandler{server.app, handleClusterList})
  9. ...
  10. }

上面这段代码来自大名鼎鼎的 linkedin 公司的 kafka 监控项目 Burrow,没有使用任何 router 框架,只使用了 net/http。只看上面这段代码似乎非常优雅,我们的项目里大概只有这五个简单的 URI,所以我们提供的服务就是下面这个样子:

  1. /
  2. /burrow/admin
  3. /v2/kafka
  4. /v2/kafka/
  5. /v2/zookeeper

如果你确实这么想的话就被骗了。我们再进 handleKafka 这个函数一探究竟:

  1. func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request) (int, string) {
  2. pathParts := strings.Split(r.URL.Path[1:], "/")
  3. if _, ok := app.Config.Kafka[pathParts[2]]; !ok {
  4. return makeErrorResponse(http.StatusNotFound, "cluster not found", w, r)
  5. }
  6. if pathParts[2] == "" {
  7. // Allow a trailing / on requests
  8. return handleClusterList(app, w, r)
  9. }
  10. if (len(pathParts) == 3) || (pathParts[3] == "") {
  11. return handleClusterDetail(app, w, r, pathParts[2])
  12. }
  13. switch pathParts[3] {
  14. case "consumer":
  15. switch {
  16. case r.Method == "DELETE":
  17. switch {
  18. case (len(pathParts) == 5) || (pathParts[5] == ""):
  19. return handleConsumerDrop(app, w, r, pathParts[2], pathParts[4])
  20. default:
  21. return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
  22. }
  23. case r.Method == "GET":
  24. switch {
  25. case (len(pathParts) == 4) || (pathParts[4] == ""):
  26. return handleConsumerList(app, w, r, pathParts[2])
  27. case (len(pathParts) == 5) || (pathParts[5] == ""):
  28. // Consumer detail - list of consumer streams/hosts? Can be config info later
  29. return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
  30. case pathParts[5] == "topic":
  31. switch {
  32. case (len(pathParts) == 6) || (pathParts[6] == ""):
  33. return handleConsumerTopicList(app, w, r, pathParts[2], pathParts[4])
  34. case (len(pathParts) == 7) || (pathParts[7] == ""):
  35. return handleConsumerTopicDetail(app, w, r, pathParts[2], pathParts[4], pathParts[6])
  36. }
  37. case pathParts[5] == "status":
  38. return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], false)
  39. case pathParts[5] == "lag":
  40. return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], true)
  41. }
  42. default:
  43. return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
  44. }
  45. case "topic":
  46. switch {
  47. case r.Method != "GET":
  48. return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
  49. case (len(pathParts) == 4) || (pathParts[4] == ""):
  50. return handleBrokerTopicList(app, w, r, pathParts[2])
  51. case (len(pathParts) == 5) || (pathParts[5] == ""):
  52. return handleBrokerTopicDetail(app, w, r, pathParts[2], pathParts[4])
  53. }
  54. case "offsets":
  55. // Reserving this endpoint to implement later
  56. return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
  57. }
  58. // If we fell through, return a 404
  59. return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
  60. }

因为默认的net/http包中的 mux 不支持带参数的路由,所以Burrow 这个项目使用了非常蹩脚的字符串 Split 和乱七八糟的 switch case 来达到自己的目的,但实际上却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个 handler 函数逻辑上较简单,最复杂的也就是这个 handleKafka。但实际上我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。

根据我们的经验,简单地来说,只要你的路由带有参数,并且这个项目的 api 数目超过了 10,就尽量不要使用 net/http 中默认的路由。在Go开源界应用最广泛的 router 是 httpRouter,很多开源的 router 框架都是基于 httpRouter 进行一定程度的改造的成果。关于 httpRouter 路由的原理,会在本章节的 router 一节中进行详细的阐释。

再来回顾一下文章开头说的,开源界有这么几种框架,第一种是对 httpRouter 进行简单的封装,然后提供定制的 middleware 和一些简单的小工具集成比如 gin,主打轻量,易学,高性能。第二种是借鉴其它语言的编程风格的一些 MVC 类框架,例如 beego,方便从其它语言迁移过来的程序员快速上手,快速开发。还有一些框架功能更为强大,除了 db 设计,大部分代码直接生成,例如 goa。不管哪种框架,适合开发者背景的就是最好的。

本章的内容除了会展开讲解 router 和 middleware 的原理外,还会以现在工程界面临的问题结合 Go 来进行一些实践性的说明。希望能够对没有接触过相关内容的读者有所帮助。