序言
在Go语言中,net/http标准库提供了基础的Web功能,如下:
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8090", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
即监听端口,映射静态路由,解析HTTP报文。
但是一些Web开发中简单的需求并不支持,需要自己手工实现。
- 动态路由:例如hello/:name,hello/*这类的规则。
- 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现。
- 模板:没有统一简化的HTML机制。
- 错误恢复:客户端异常请求可能导致Web服务崩溃退出
zgin框架
我们会使用Go语言实现一个简单的Web框架,起名叫做zgin,没错它是参照模仿大名鼎鼎的Gin来实现的,Gin的代码总共是14K,其中测试代码9K,也就是说实际代码量只有5K。Gin是我非常喜欢的一个框架,与Python中的Flask很像,小而美。
HTTP封装
Go语言内置了 net/http库,封装了HTTP网络编程的基础的接口,我们实现的Gee Web 框架便是基于net/http的。我们接下来通过一个例子,简单介绍下这个库的使用。
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/header", zheader)
log.Fatal(http.ListenAndServe(":9080", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.PATH = %q\n", r.URL.Path)
}
func zheader(w http.ResponseWriter, r *http.Request) {
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
}
我们设置了2个路由,/和/header,分别绑定 handler和 zheader , 根据不同的HTTP请求会调用不同的处理函数。访问/,响应是URL.Path = /,而/hello的响应则是请求头(header)中的键值对信息。
测试:
curl localhost:9080/
URL.PATH = "/"
curl localhost:9080/zhangxh
URL.PATH = "/zhangxh"
curl localhost:9080/header
Header["User-Agent"] = ["curl/7.79.1"]
Header["Accept"] = ["*/*"]
main 函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:9080表示在 9080 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。
http.Handler接口
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
ListenAndServe函数的第二个参数的类型是什么呢?通过查看net/http的源码可以发现,Handler是一个接口,该接口拥有一个ServeHTTP 方法,也就是说,只要传入任何实现了 ServerHTTP 方法的实例,所有的HTTP请求,就都交给了该实例处理了。我们可以测试一下:
package main
import (
"fmt"
"log"
"net/http"
)
type Engine struct {
}
func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
case "/header":
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(":9080", engine))
}
- 我们定义了一个空的结构体Engine,实现了方法ServeHTTP。这个方法有2个参数,第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 ResponseWriter ,利用 ResponseWriter 可以构造针对该请求的响应。
- 在 main 函数中,我们给 ListenAndServe 方法的第二个参数传入了刚才创建的engine实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现Engine之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
- 运行代码结果与测试与之前是一致的响应
zgin框架的雏形
zgin
├── go.mod
├── main.go
└── zgin
└── zgin.go
zgin/main.go
package main
import (
"fmt"
"net/http"
"zgin/zgin"
)
func main() {
r := zgin.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(":9080")
}
看到这里,如果你使用过gin框架的话,肯定会觉得无比的亲切。gee框架的设计以及API均参考了gin。使用New()创建 gee 的实例,使用 GET()方法添加路由,最后使用Run()启动Web服务。这里的路由,只是静态路由,不支持/hello/:name这样的动态路由,动态路由将在后面实现。
zgin/zgin/zgin.go
package zgin
import (
"fmt"
"net/http"
)
// 定义HandlerFunc为func(w http.ResponseWriter,r *http.Request)函数类型
type HandlerFunc func(w http.ResponseWriter, r *http.Request)
// Engine implement the interface of ServeHTTP
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
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
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)
}
}
那么zgin.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 工具访问,结果与最开始的一致。

Context上下文
对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
用返回 JSON 数据作比较,感受下封装前后的差距。封装前:
obj = map[string]interface{
}{
"name": "geektutu",
"password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
http.Error(w, err.Error(), 500)
}
VS 封装后:
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm

最低0.47元/天 解锁文章
946

被折叠的 条评论
为什么被折叠?



