title: Go语言动手写Web框架 - Gee第一天 http.Handler
date: 2019-08-12 00:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了Go标准库 net/http 和 http.Handler 接口的使用,拦截所有的 HTTP 请求,交给Gee框架处理。
tags:

  • Go
    nav: 从零实现
    categories:
  • Web框架 - Gee
    keywords:
  • Go语言
  • 从零实现Web框架
  • 动手写Web框架
  • net/http
    image: post/gee/gee.jpg
    github: https://github.com/geektutu/7days-golang
    book: 七天用Go从零实现系列
    book_title: Day1 HTTP 基础

本文是 7天用Go从零实现Web框架Gee教程系列的第一篇。

  • 简单介绍net/http库以及http.Handler接口。
  • 搭建Gee框架的雏形,代码约50行

标准库启动Web服务

Go语言内置了 net/http库,封装了HTTP网络编程的基础的接口,我们实现的Gee Web 框架便是基于net/http的。我们接下来通过一个例子,简单介绍下这个库的使用。

day1-http-base/base1/main.go

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. http.HandleFunc("/", indexHandler)
  9. http.HandleFunc("/hello", helloHandler)
  10. log.Fatal(http.ListenAndServe(":9999", nil))
  11. }
  12. // handler echoes r.URL.Path
  13. func indexHandler(w http.ResponseWriter, req *http.Request) {
  14. fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
  15. }
  16. // handler echoes r.URL.Header
  17. func helloHandler(w http.ResponseWriter, req *http.Request) {
  18. for k, v := range req.Header {
  19. fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
  20. }
  21. }

我们设置了2个路由,//hello,分别绑定 indexHandlerhelloHandler , 根据不同的HTTP请求会调用不同的处理函数。访问/,响应是URL.Path = /,而/hello的响应则是请求头(header)中的键值对信息。

用 curl 这个工具测试一下,将会得到如下的结果。

  1. $ curl http://localhost:9999/
  2. URL.Path = "/"
  3. $ curl http://localhost:9999/hello
  4. Header["Accept"] = ["*/*"]
  5. Header["User-Agent"] = ["curl/7.54.0"]

main 函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:9999表示在 9999 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。

实现http.Handler接口

  1. package http
  2. type Handler interface {
  3. ServeHTTP(w ResponseWriter, r *Request)
  4. }
  5. func ListenAndServe(address string, h Handler) error

第二个参数的类型是什么呢?通过查看net/http的源码可以发现,Handler是一个接口,需要实现方法 ServeHTTP ,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。马上来试一试吧。

day1-http-base/base2/main.go

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. // Engine is the uni handler for all requests
  8. type Engine struct{}
  9. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  10. switch req.URL.Path {
  11. case "/":
  12. fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
  13. case "/hello":
  14. for k, v := range req.Header {
  15. fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
  16. }
  17. default:
  18. fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
  19. }
  20. }
  21. func main() {
  22. engine := new(Engine)
  23. log.Fatal(http.ListenAndServe(":9999", engine))
  24. }
  • 我们定义了一个空的结构体Engine,实现了方法ServeHTTP。这个方法有2个参数,第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 ResponseWriter ,利用 ResponseWriter 可以构造针对该请求的响应。

  • main 函数中,我们给 ListenAndServe 方法的第二个参数传入了刚才创建的engine实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现Engine之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

  • 代码的运行结果与之前的是一致的。

Gee框架的雏形

我们接下来重新组织上面的代码,搭建出整个框架的雏形。

最终的代码目录结构是这样的。

  1. gee/
  2. |--gee.go
  3. |--go.mod
  4. main.go
  5. go.mod

go.mod

day1-http-base/base3/go.mod

  1. module example
  2. go 1.13
  3. require gee v0.0.0
  4. replace gee => ./gee
  • go.mod 中使用 replace 将 gee 指向 ./gee

从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。

main.go

day1-http-base/base3/main.go

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "gee"
  6. )
  7. func main() {
  8. r := gee.New()
  9. r.GET("/", func(w http.ResponseWriter, req *http.Request) {
  10. fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
  11. })
  12. r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
  13. for k, v := range req.Header {
  14. fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
  15. }
  16. })
  17. r.Run(":9999")
  18. }

看到这里,如果你使用过gin框架的话,肯定会觉得无比的亲切。gee框架的设计以及API均参考了gin。使用New()创建 gee 的实例,使用 GET()方法添加路由,最后使用Run()启动Web服务。这里的路由,只是静态路由,不支持/hello/:name这样的动态路由,动态路由我们将在下一次实现。

gee.go

day1-http-base/base3/gee/gee.go

  1. package gee
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. // HandlerFunc defines the request handler used by gee
  7. type HandlerFunc func(http.ResponseWriter, *http.Request)
  8. // Engine implement the interface of ServeHTTP
  9. type Engine struct {
  10. router map[string]HandlerFunc
  11. }
  12. // New is the constructor of gee.Engine
  13. func New() *Engine {
  14. return &Engine{router: make(map[string]HandlerFunc)}
  15. }
  16. func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
  17. key := method + "-" + pattern
  18. engine.router[key] = handler
  19. }
  20. // GET defines the method to add GET request
  21. func (engine *Engine) GET(pattern string, handler HandlerFunc) {
  22. engine.addRoute("GET", pattern, handler)
  23. }
  24. // POST defines the method to add POST request
  25. func (engine *Engine) POST(pattern string, handler HandlerFunc) {
  26. engine.addRoute("POST", pattern, handler)
  27. }
  28. // Run defines the method to start a http server
  29. func (engine *Engine) Run(addr string) (err error) {
  30. return http.ListenAndServe(addr, engine)
  31. }
  32. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  33. key := req.Method + "-" + req.URL.Path
  34. if handler, ok := engine.router[key]; ok {
  35. handler(w, req)
  36. } else {
  37. fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
  38. }
  39. }

那么gee.go就是重头戏了。我们重点介绍一下这部分的实现。

  • 首先定义了类型HandlerFunc,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine中,添加了一张路由映射表router,key 由请求方法和静态路由地址构成,例如GET-/GET-/helloPOST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。

  • 当用户调用(*Engine).GET()方法时,会将路由和处理方法注册到映射表 router 中,(*Engine).Run()方法,是 ListenAndServe 的包装。

  • Engine实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND

执行go run main.go,再用 curl 工具访问,结果与最开始的一致。

  1. $ curl http://localhost:9999/
  2. URL.Path = "/"
  3. $ curl http://localhost:9999/hello
  4. Header["Accept"] = ["*/*"]
  5. Header["User-Agent"] = ["curl/7.54.0"]
  6. curl http://localhost:9999/world
  7. 404 NOT FOUND: /world

至此,整个Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。当然,到目前为止,我们还没有实现比net/http标准库更强大的能力,不用担心,很快就可以将动态路由、中间件等功能添加上去了。