本文是 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package  mainimport  (	"fmt"  	"log"  	"net/http"  ) func  main ()   {	http.HandleFunc("/" , indexHandler) 	http.HandleFunc("/hello" , helloHandler) 	log.Fatal(http.ListenAndServe(":9999" , nil )) } func  indexHandler (w http.ResponseWriter, req *http.Request)   {	fmt.Fprintf(w, "URL.Path = %q\n" , req.URL.Path) } func  helloHandler (w http.ResponseWriter, req *http.Request)   {	for  k, v := range  req.Header { 		fmt.Fprintf(w, "Header[%q] = %q\n" , k, v) 	} } 
 
 
我们设置了2个路由,/和/hello,分别绑定 indexHandler  和 helloHandler  , 根据不同的HTTP请求会调用不同的处理函数。访问/,响应是URL.Path = /,而/hello的响应则是请求头(header)中的键值对信息。
用 curl 这个工具测试一下,将会得到如下的结果。
1 2 3 4 5 $ curl http://localhost:9999/ URL.Path = "/"  $ curl http://localhost:9999/hello Header["Accept" ] = ["*/*" ] Header["User-Agent" ] = ["curl/7.54.0" ] 
 
main  函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:9999表示在 9999  端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。
实现http.Handler接口 1 2 3 4 5 6 7 package  httptype  Handler interface  {    ServeHTTP(w ResponseWriter, r *Request) } func  ListenAndServe (address string , h Handler)   error 
 
第二个参数的类型是什么呢?通过查看net/http的源码可以发现,Handler是一个接口,需要实现方法 ServeHTTP  ,也就是说,只要传入任何实现了 ServerHTTP  接口的实例,所有的HTTP请求,就都交给了该实例处理了。马上来试一试吧。
day1-http-base/base2/main.go 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package  mainimport  (	"fmt"  	"log"  	"net/http"  ) type  Engine struct {}func  (engine *Engine)   ServeHTTP(w http.ResponseWriter, req *http.Request) {	switch  req.URL.Path { 	case  "/" : 		fmt.Fprintf(w, "URL.Path = %q\n" , req.URL.Path) 	case  "/hello" : 		for  k, v := range  req.Header { 			fmt.Fprintf(w, "Header[%q] = %q\n" , k, v) 		} 	default : 		fmt.Fprintf(w, "404 NOT FOUND: %s\n" , req.URL) 	} } func  main ()   {	engine := new (Engine) 	log.Fatal(http.ListenAndServe(":9999" , engine)) } 
 
 
我们定义了一个空的结构体Engine,实现了方法ServeHTTP。这个方法有2个参数,第二个参数是 Request  ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 ResponseWriter  ,利用 ResponseWriter  可以构造针对该请求的响应。 
在 main  函数中,我们给 ListenAndServe  方法的第二个参数传入了刚才创建的engine实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现Engine之前,我们调用 http.HandleFunc  实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。 
代码的运行结果与之前的是一致的。 
 
Gee框架的雏形 我们接下来重新组织上面的代码,搭建出整个框架的雏形。
最终的代码目录结构是这样的。
1 2 3 4 5 gee/   |--gee.go   |--go.mod main.go go.mod 
 
 
go.mod day1-http-base/base3/go.mod 
1 2 3 4 5 6 7 module example go  1.13 require gee v0.0 .0  replace gee => ./gee 
 
在 go.mod 中使用 replace 将 gee 指向 ./gee 
 
从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。
 
main.go day1-http-base/base3/main.go 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package  mainimport  (	"fmt"  	"net/http"  	"gee"  ) func  main ()   {	r := gee.New() 	r.GET("/" , func (w http.ResponseWriter, req *http.Request)   { 		fmt.Fprintf(w, "URL.Path = %q\n" , req.URL.Path) 	}) 	r.GET("/hello" , func (w http.ResponseWriter, req *http.Request)   { 		for  k, v := range  req.Header { 			fmt.Fprintf(w, "Header[%q] = %q\n" , k, v) 		} 	}) 	r.Run(":9999" ) } 
 
看到这里,如果你使用过gin框架的话,肯定会觉得无比的亲切。gee框架的设计以及API均参考了gin。使用New()创建 gee 的实例,使用 GET()方法添加路由,最后使用Run()启动Web服务。这里的路由,只是静态路由,不支持/hello/:name这样的动态路由,动态路由我们将在下一次实现。
gee.go day1-http-base/base3/gee/gee.go 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package  geeimport  (	"fmt"  	"net/http"  ) type  HandlerFunc func (http.ResponseWriter, *http.Request) type  Engine struct  {	router map [string ]HandlerFunc } func  New ()   *Engine {	return  &Engine{router: make (map [string ]HandlerFunc)} } func  (engine *Engine)   addRoute(method string , pattern string , handler HandlerFunc) {	key := method + "-"  + pattern 	engine.router[key] = handler } func  (engine *Engine)   GET(pattern string , handler HandlerFunc) {	engine.addRoute("GET" , pattern, handler) } func  (engine *Engine)   POST(pattern string , handler HandlerFunc) {	engine.addRoute("POST" , pattern, handler) } func  (engine *Engine)   Run(addr string ) (err error ) {	return  http.ListenAndServe(addr, engine) } func  (engine *Engine)   ServeHTTP(w http.ResponseWriter, req *http.Request) {	key := req.Method + "-"  + req.URL.Path 	if  handler, ok := engine.router[key]; ok { 		handler(w, req) 	} else  { 		fmt.Fprintf(w, "404 NOT FOUND: %s\n" , req.URL) 	} } 
 
 
那么gee.go就是重头戏了。我们重点介绍一下这部分的实现。
首先定义了类型HandlerFunc,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine中,添加了一张路由映射表router,key 由请求方法和静态路由地址构成,例如GET-/、GET-/hello、POST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。 
当用户调用(*Engine).GET()方法时,会将路由和处理方法注册到映射表 router  中,(*Engine).Run()方法,是 ListenAndServe  的包装。 
Engine实现的 ServeHTTP  方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND  。 
 
执行go run main.go,再用 curl  工具访问,结果与最开始的一致。
1 2 3 4 5 6 7 $ curl http://localhost:9999/ URL.Path = "/"  $ curl http://localhost:9999/hello Header["Accept" ] = ["*/*" ] Header["User-Agent" ] = ["curl/7.54.0" ] curl http://localhost:9999/world 404 NOT FOUND: /world 
 
 
至此,整个Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。当然,到目前为止,我们还没有实现比net/http标准库更强大的能力,不用担心,很快就可以将动态路由、中间件等功能添加上去了。