Go语言内置了 net/http
库,封装了HTTP网络编程的基础的接口。这个Web 框架便是基于net/http
的。我们先回顾下这个库的使用。
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
//可以写成匿名函数(lambda表达式),handler echoes r.URL.Path
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
})
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe("localhost:10000", nil))
//log.Fatal(http.ListenAndServe(":10000", nil)) 也可以写成这样的,没有写到ip
}
// handler echoes r.URL.Header
func helloHandler(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
}
来看看效果
这里设置了两个路由 / 和hello,分别绑定了一个匿名函数和helloHandler。之后根据不同的http请求会调用不同的处理函数。结果如上如图。
main 函数的最后一行,是用来启动 Web 服务的。
第一个参数是ip地址,localhost:110000表示在 本地地址的10000端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的默认实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。
来看看源码
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
//...............
serverHandler{c.server}.ServeHTTP(w, w.req)
//.................
}
通过源码可以看到,Handler是一个接口类型,需要实现ServeHTTP方法。
http.ListenAndServe中会调用ServerHTTP方法,即是调用h.ServerHTTP。
即是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。所以我们要自己写一个实现该接口的实例,让所有的HTTP请求由自己来写。
那接下我们实现一个Handler。
package main
import (
"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 := &Engine{}
log.Fatal(http.ListenAndServe("localhost:10000", engine))
}
我们定义了一个空的结构体Engine,实现了方法ServeHTTP。
ServeHTTP方法有两个参数,一是ResponseWriter,这个是用来构建对客户端的http回复响应。二是Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、请求Header和Body等信息。
而在main函数中,我们给ListenAndServe方法的第二个参数传入了engine实例。至此,我们走出了实现web框架的第一步,即是将所有的HTTP请求都转向了我们自己的处理逻辑。
而在实现Enginx之前,我们调用 http.HandleFunc实现了路由和helloHandler的映射,也就是只能针对具体的路由来写对应的处理逻辑。而在实现Engine之后,我们可以拦截所有的HTTP请求,拥有了统一的控制入口。这样我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
我们再整理之前所讲的,搭建出整个框架的雏形。
进行初始化
主要文件是main.go和gee.go
先看下是如何使用的
package main
import (
"fmt"
"geeV1/gee"
"net/http"
)
func main() {
engine := gee.New()
engine.GET("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
})
engine.POST("/hello", func(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
})
engine.Run("localhost:10000")
}
和gin
框架的使用是相似的。使用New()
创建 gee 的实例,使用 GET(),POST()
方法添加路由,最后使用Run()
启动Web服务。这里的路由,只是静态路由,不支持/hello/:name
这样的动态路由,之后会实现动态路由。
接下来看看是如何实现的
package gee
import (
"fmt"
"net/http"
)
type HandlerFunc func(http.ResponseWriter, *http.Request)
type Engine struct {
router map[string]HandlerFunc //添加对应的路由和路由函数的映射
}
// 创建enginx实例
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) error {
return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
fmt.Println(key)
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
w.WriteHeader(http.StatusNotFound) //添加状态码,404
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL.Path)
}
}
那么gee.go
就是重头戏了。我们重点介绍一下这部分的实现。
对比之前的空结构体Engine,其结构体添加了router 路由映射表。
key 由请求方法和静态路由地址构成,例如GET-/
、GET-/hello
、POST-/hello
,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法(用户自己写的)。
main函数中使用GET()等添加路由,其GET()等方法就是调用了addRoute,把路由和对应的handler添加到router中。
Run方法是调用了http.ListenAndServe(addr, engine)。所有的HTTP请求都会首先进入ServeHTTP中。Engine
实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND 。
执行go run main.go查看效果
至此,整个web框架的原型已经出来了。我们实现了路由映射表,提供了用户注册静态路由的方法,也包装了启动服务的函数。当然,当前我们实现的功能也没有比net/http
标准库更强大。之后,会陆续将动态路由、中间件等功能添加上去。
完整代码:https://github.com/liwook/Go-projects/tree/main/gee-web/1-http-base