仿照Go web框架gin手写自己的web框架 【上】
主要目的是学习Go web服务器的构成原理,方便工作开发。
本文内容主要是参考了
学习目标,构建一个类似gin
的框架 gee
,当然学习的话,只用包含最简单的几个核心功能就可以了,比如路由分组,中间件,异常恢复等。
最终构成的代码结构如下:
gee/ // 自定义gee框架
|-- gee.go // 核心文件 封装net/http
|-- xxx.go // 其他扩展文件
|-- ...
main.go // 用户web服务代码 引入gee框架
go.mod
Go标准库 net/http
package main
import (
"fmt"
"net/http"
)
func main() {
// 设置路由 以及 请求处理函数
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte("hello world"))
})
// 设置服务地址
server := http.Server{
Addr: "127.0.0.1:7050",
}
// 启动监听服务
_ = server.ListenAndServe()
}
以上是一个最简单的示例,性能也绝对不差,为什么我会肯定性能不差了?因为Go标准库 net/http
库已经把主要的部分封装的非常强悍了。现有的大部分框架(如gin)都是基于标准库,然后自己封装一层wrapper
,常见的功能有
路由分组,中间件,异常机制等核心功能。
可以从server.ListenAndServe()
看到核心服务函数
https://github.com/golang/go/blob/e491c6eea9ad599a0ae766a3217bd9a16ca3a25a/src/net/http/server.go#L2951
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
baseCtx := context.Background()
if srv.BaseContext != nil {
baseCtx = srv.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
// 最外层死循环
for {
// 监听listener的请求
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx) // 每新进来一个连接,就开一个 goroutine 处理
}
}
每个 goroutine 最小只需要2K的内存,这也是go并发性能的保证。
https://github.com/golang/go/blob/bbd25d26c0a86660fb3968137f16e74837b7a9c6/src/runtime/stack.go#L72
如果当前 HTTP 服务接收到了海量的请求,会在内部创建大量的 Goroutine,这可能会使整个服务质量明显降低无法处理请求。
但是有一个第三方库宣称比 net/http
快10倍。fasthttp
关于`fasthttp`的简单介绍以及常见问题
对比测试设备以及结果
https://github.com/valyala/fasthttp/issues/4
为什么fasthttp 比 net/http 快10倍?
https://stackoverflow.com/questions/41627931/why-is-fasthttp-faster-than-net-http
基于 fasthttp
,就诞生了 如 https://github.com/gofiber/fiber 的框架。
关于为什么 gin
框架 不使用 fasthttp
替换标准库 net/http
的问题?
https://github.com/gin-gonic/gin/issues/498
使用实例化Handler
接口的方式构建web服务
上面一种方式,扩展方式不好,路由控制什么不好再次封装,于是可以采用以下方式扩展。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func ListenAndServe(address string, h Handler) error
这种是实现 Handler接口 ServeHTTP(ResponseWriter, *Request) 的方式启动服务。
该接口只定义了一个方法,只要一个类型实现了这个 ServeHTTP 方法,
就可以把该类型的变量直接赋值给 Handler接口,以下demo就是基于这种方式实现。
Go中的接口,是隐式的实现,只要实现了接口的所有方法,就是实现了接口,不需要显示的实现,也就是常说的鸭子类型。
此片段GitHub地址
package main
import (
"fmt"
"net/http"
)
type Engine struct {
router router
}
// 定义请求处理函数类型
type handler func(w http.ResponseWriter, r *http.Request)
// 定义路由 - 对应 处理请求函数
type router map[string]handler
func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 只通过路径 匹配处理请求的方法 不区分 GET or POST
// r.URL.Path
if handler, ok := engine.router[r.URL.Path]; ok {
handler(w, r)
} else {
_, _ = fmt.Fprintf(w, "404")
}
}
func hello(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Hello World")
}
func main() {
r := router{}
r["/"] = func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "首页")
}
r["/hello"] = hello
//engine := new(Engine)
engine := &Engine{
router: r,
}
addr := "127.0.0.1:7051"
fmt.Println("服务启动:", addr)
_ = http.ListenAndServe(addr, engine)
}
学习总结
总而言之呢,就是Go net/http
标准库已经很难强大了,自己只用封装一些wrapper
就足够了,第二个实例化Handler
接口方式例子 是后面模仿gin
框架的基础,需要明白Go基础关于接口的知识,才好理解这个demo。
如果对接口不熟可以参考 https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/