赞
踩
本教程参考:七天用Go从零实现Web框架Gee教程
要开发一个Web应用,我们首先会考虑框架的选型,对于Javaer来说,通常Spring mvc是Web开发的首选框架;
如果使用GoLang进行开发,可能会选择Beego,Gin,Iris等,但是无论是哪一种Web框架其设计思想都是类似的。
在我初学GoLang的net/http标准库时,我觉得标准库提供的功能已经帮助我们封装好了很多细节,例如: http协议的解析,请求参数的提取等等…
我们完全可以直接利用net/http标准库进行简单的web程序开发,那为什么还需要在标准库上再封装上一层皮呢?
直接利用GoLang中的net/http标准库开发,就像直接使用Java中的servlet开发web应用一样,虽然可以,但是太多重复的关注点没有被抽取出来,会导致程序代码臃肿,难以维护。
我们先看看标准库net/http如何处理一个请求。
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
net/http提供了基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。一些Web开发中简单的需求并不支持,需要手工实现。
当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。但并不是每一个频繁处理的地方都适合在框架中完成。Python有一个很著名的Web框架,名叫bottle,整个框架由bottle.py一个文件构成,共4400行,可以说是一个微框架。那么理解这个微框架提供的特性,可以帮助我们理解框架的核心能力。
这个教程将使用 Go 语言实现一个简单的 Web 框架,起名叫做Geo。 该框架设计参考的是极客兔兔编写的Gee教程。
废话不多说,咱们直接开始。
Go语言内置了 net/http库,封装了HTTP网络编程的基础的接口,我们实现的Geo Web 框架便是基于net/http的。我们接下来通过一个例子,简单介绍下这个库的使用。
package main import ( "fmt" "log" "net/http" ) func main() { //注册静态路由 // “/“作为兜底处理器 http.HandleFunc("/", indexHandler) //只拦截精确匹配到的/hello请求 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 这个工具测试一下,将会得到如下的结果。
main 函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:9999表示在 9999 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
第二个参数的类型是什么呢?
通过查看net/http的源码可以发现,Handler是一个接口,需要实现方法 ServeHTTP ,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。
这是不是非常类似于Spring mvc对servlet进行的封装,servlet原生开发是一个servlet处理一个请求,而spring对此的处理则是,提供了一个统一入口DispathcerServlet入口,所有请求通通被该servlet拦截,然后执行后续请求派发操作。
马上来试一试吧。
package main import ( "fmt" "log" "net/http" ) // Engine is the uni handler for all requests //可以类比spring mvc中的DispathcerServlet type Engine struct{} //类比spring mvc中DispathcerServlet提供的入口方法Dispatch 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)) }
我们接下来重新组织上面的代码,搭建出整个框架的雏形。
最终的代码目录结构是这样的。
geo/
|--geo.go
|--go.mod
main.go
go.mod
module main
go 1.18
require geo v0.0.0
replace geo => ./geo
从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。
package main import ( "fmt" "net/http" //导入自定义包 "geo" ) func main() { r := geo.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框架的话,肯定会觉得无比的亲切。geo框架的设计以及API均参考了gin。使用New()创建 gee 的实例,使用 GET()方法添加路由,最后使用Run()启动Web服务。这里的路由,只是静态路由,不支持/hello/:name这样的动态路由,动态路由我们将在下一次实现。
package geo import ( "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 { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL) } }
那么geo.go就是重头戏了。我们重点介绍一下这部分的实现。
执行go run main.go,再用 curl 工具访问,结果与最开始的一致。
$ 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
至此,整个Geo框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。当然,到目前为止,我们还没有实现比net/http标准库更强大的能力,不用担心,很快就可以将动态路由、中间件等功能添加上去了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。