仿照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](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框架系列代码地址


文章作者: 王小右
版权声明: 咳咳想白嫖文章?本文章著作权归作者所有,任何形式的转载都请注明出处。 https://www.charmcode.cn !
还能输入 100 字
  目录