代码如下:

package mainimport (    "context"    "log"    "net/http"    "os"    "os/signal"    "sync"    "time"    "github.com/gin-gonic/gin")var wg sync.WaitGroupvar apiQuit = make(chan bool)func apiQuitSignal() {    log.Println("quit signal")    apiQuit <- true}func main() {    router := gin.New()    router.GET("/quit", func(c *gin.Context) {        log.Println("GET /quit")        apiQuitSignal()        //time.AfterFunc(5*time.Second, apiQuitSignal)        c.String(200, "quit")        //c.String(200, "quit in 5 seconds")    })    router.GET("/hello", func(c *gin.Context) {        log.Println("GET /hello")        c.String(200, "hello")    })    srv := &http.Server{        Addr:    ":8888",        Handler: router,    }    register := func(f func()) {        wg.Add(1)        srv.RegisterOnShutdown(func() {            defer wg.Done()            f()        })    }    register(func() {        time.Sleep(10 * time.Second)    })    go func() {        // 服务连贯        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {            log.Fatalf("listen: %s\n", err)        }    }()    quit := make(chan os.Signal)    signal.Notify(quit, os.Interrupt)    select {    case <-quit:        log.Println("quit from os.Signal")    case <-apiQuit:        log.Println("quit from api")    }    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)    //ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)    defer cancel()    if err := srv.Shutdown(ctx); err != nil {        log.Fatal("Server Shutdown:", err)    }    wg.Wait()    log.Println("Server Exit")}

下面的服务应用apiQuit从申请quit时取得shutdown的信号,而后进行graceful shutdown操作。

而后在申请的过程中,呈现了一些奇怪的景象,表述如下:

一,在服务器启动后,仅申请quit接口,会报
Server Shutdown:context deadline exceeded
这个显著是srv.Shutdown(ctx)的时候context超时了。

二,在服务器启动后,先申请hello接口,再申请quit接口,能够失常退出。

三,在服务器启动后,先申请quit接口,再立即申请hello接口,能够失常退出。

四,减少context的超时工夫到10秒,能够失常退出。

这显著是Shutdown的代码有些奇怪的feature(或者bug)。

看代码:

// file: net/http/server.gofunc (srv *Server) Shutdown(ctx context.Context) error {    srv.inShutdown.setTrue()    srv.mu.Lock()    lnerr := srv.closeListenersLocked()    srv.closeDoneChanLocked()    for _, f := range srv.onShutdown {        go f()    }    srv.mu.Unlock()    pollIntervalBase := time.Millisecond    nextPollInterval := func() time.Duration {        // Add 10% jitter.        interval := pollIntervalBase + time.Duration(rand.Intn(int(pollIntervalBase/10)))        // Double and clamp for next time.        pollIntervalBase *= 2        if pollIntervalBase > shutdownPollIntervalMax {            pollIntervalBase = shutdownPollIntervalMax        }        return interval    }    timer := time.NewTimer(nextPollInterval())    defer timer.Stop()    for {        if srv.closeIdleConns() && srv.numListeners() == 0 {            return lnerr        }        select {        case <-ctx.Done():            return ctx.Err()        case <-timer.C:            timer.Reset(nextPollInterval())        }    }}func (s *Server) closeIdleConns() bool {    s.mu.Lock()    defer s.mu.Unlock()    quiescent := true    for c := range s.activeConn {        st, unixSec := c.getState()        // Issue 22682: treat StateNew connections as if        // they're idle if we haven't read the first request's        // header in over 5 seconds.        if st == StateNew && unixSec < time.Now().Unix()-5 {            st = StateIdle        }        if st != StateIdle || unixSec == 0 {            // Assume unixSec == 0 means it's a very new            // connection, without state set yet.            quiescent = false            continue        }        c.rwc.Close()        delete(s.activeConn, c)    }    return quiescent}

先找到Shutdown函数,看到ctx.Done(),超时报错在这里,而后定位srv.closeIdleConns()
再找到closeIdleConns办法,能够看到次要代码就是一个
for c := range s.activeConn
而后留神上面的代码

// Issue 22682: treat StateNew connections as if// they're idle if we haven't read the first request's// header in over 5 seconds.if st == StateNew && unixSec < time.Now().Unix()-5 {    st = StateIdle}

如果连贯的状态是StateNew的时候,会提早到5秒(实际上并不是严格的5秒,参考Shutdown函数里的timer机制)能力转为StateIdle。
并且这个问题曾经有人提到了,参考Issue 22682
https://github.com/golang/go/issues/22682

解决的办法也就很简略:
办法一、在quit申请完结5秒后再发信号之后再调用Shutdown
办法二、在发送了quit信号后阻塞5秒再调用Shutdown