仿照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服务

上面一种方式,扩展方式不好,路由控制什么不好再次封装,于是可以采用以下方式扩展。

https://github.com/golang/go/blob/e491c6eea9ad599a0ae766a3217bd9a16ca3a25a/src/net/http/server.go#L86

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/

模仿ginweb框架系列代码地址