SoFunction
Updated on 2025-03-04

In-depth understanding of the elegant closure of the Go standard library http server

introduction

This article is the third article in the series [In-depth understanding of Go Standard Library]

Article 1:Starting of http server

Article 2:ServeMux usage and pattern matching

Article 3: Elegant closure of http server👈

This series will be updated continuously, welcome to follow 👏 Get real-time notifications

Remember how to start an HTTP Server?

package main

import (
 "net"
 "net/http"
)

func main() {
 // Method 1 err := (":8080", nil)
 if err != nil {
   panic(err)
 }
    
 // Method 2 // server := &{Addr: ":8080"}
 // err := ()
 // if err != nil {
 //  panic(err)
 // }
}

ListenAndServeWithout errors, it will be blocked in this location. How to stop such an HTTP Server?

CTRL+Cis a common way to end a process, andkill pidorkill -l 15 pidThere is no difference in commands in essence, they are all sent to the processSIGTERMSignal. Because the program is not setSIGTERMsignal handler, so the system default signal handler ends our process

What problems will this bring?

Our server may be processing the request and not completing when the server's process is killed. Therefore, an unexpected error was generated for the client

curl -v --max-time 4 127.0.0.1:8009/foo
* Connection #0 to host 127.0.0.1 left intact
*   Trying 127.0.0.1:8009...
* Connected to 127.0.0.1 (127.0.0.1) port 8009 (#0)
> GET /foo HTTP/1.1
> Host: 127.0.0.1:8009
> User-Agent: curl/7.86.0
> Accept: */*
> 
* Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server

If there is a nginx proxy, nginx will generate a 502 response because of the interruption of upstream.

curl -v --max-time 11 127.0.0.1:8010/foo
*   Trying 127.0.0.1:8010...
* Connected to 127.0.0.1 (127.0.0.1) port 8010 (#0)
> GET /foo HTTP/1.1
> Host: 127.0.0.1:8010
> User-Agent: curl/7.86.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 502 Bad Gateway
< Server: nginx/1.25.3
< Date: Sat, 02 Dec 2023 10:14:33 GMT
< Content-Type: text/html
< Content-Length: 497
< Connection: keep-alive
< ETag: "6537cac7-1f1"

The initial implementation of elegant closure

Graceful shutdown refers to the fact that our HTTP Server not only rejects new requests before shutting down, but also correctly handles the ongoing requests, and then the process exits. How to achieve it?

🌲 Start HTTP server asynchronously

becauseListenAndServeIt will block the goroutine. If we still need to let the code continue to execute, we need to put it into an asynchronous goroutine

go func() {
    if err := (); err != nil {
        panic(err)
    }
}()

🌲 Step 2: Set up SIGTERM signal processor

The default signal processor of the operating system is to directly end the process, so to implement graceful shutdown, you need to set up the program's own signal processor.

In Go, the following method can be used to process signals.

  • To set the signal we want to monitor. Once a signal set by the program occurs, the signal will be written to the channel.

  • signalCh chan We define a buffered channel, and the read operation will block when there is no data in the channel

signalCh := make(chan , 1)
(signalCh, , )

sig := &lt;-signalCh
("Received signal: %v\n", sig)

🌲 Step 3: Smoothly shut down HTTP Server

What to handle in a custom signal handler?

1. First, you need to close the listening of the port. At this time, the new request cannot establish a connection.

2. Close the idle connection

3. Waiting for the in-progress connection is completed and closed after it becomes an idle connection.

Before Go 1.8, implementing the above operations required a lot of code to be written, and there are also some third-party libraries (tylerstillwate/graceful, facebookarchive/grace, etc.) available for use. But after Go1.8, the standard library providesShutdown()method

🌲 Implementation: The above three steps are as follows

func main() {
 mx := ()
 ("/foo", func(w , r *) {
  (((10)) * )
  ([]byte("Receive path foo\n"))
 })

 srv := {
  Addr:    ":8009",
  Handler: mx,
 }

 go func() {
  if err := (); err != nil {
   panic(err)
  }
 }()

 signalCh := make(chan , 1)
 (signalCh, , )

 sig := <-signalCh
 ("Received signal: %v\n", sig)

 if err := (()); err != nil {
  ("Server shutdown failed: %v\n", err)
 }

 ("Server shutdown gracefully")
}

Not receivedSIGINTSIGTERMBefore the signal, the main goroutine issignalChRead blocking

Once the signal is received,signalChThe blocking is cancelled and the server will be executed downShutdown()Shutdown()The function handles active and inactive connections and returns the result

Is there any problem with the above code?

Elegantly close the implementation details

🌲 WhenShutdownWhen calledListenAndServeWill return immediatelyError

go func() {
    if err := (); err != nil {
        panic(err)
    }
}()

For the above code,Shutdown()Just called,ListenAndServeThe goroutine where it is located throws panic, which also causes the main goroutine to be exited and not reach the run.Shutdown()Expected results

If you still want to be rightListenAndServeThe error throws painc and needs to be ignoredError

go func() {
    err := ()
    if err != nil &amp;&amp; err !=  {
        panic(err)
    }
}()

🌲 Shut down the server for a limited time

During the elegant closing process, wait for the in-progress request to complete. However, the process of request processing may be very time-consuming, or the request itself has fallen into an indefinitely state, so it is impossible for us to wait infinitely, so it is safer to set a closed upper limit time.

Shutdown()Accept oneType parameters, we can use to set the timeout time

ctx, cancel := ((), 5*)
defer cancel()

if err := (ctx); err != nil {
    ("Server shutdown failed: %v\n", err)
}

("Server shutdown gracefully")

pass()It can distinguish whether the server shutdown is caused by timeout, so different reasons for exit

ctx, cancel := ((), 5*)
defer cancel()
if err := (ctx); err != nil {
    select {
        case &lt;-():
        // Since the timeout time is reached, the server is closed, the elegant shutdown is not completed        ("timeout of 5 seconds.")
        default:
        // Service shutdown exception caused by other reasons, elegant shutdown is not completed        ("Server shutdown failed: %v\n", err)
    }
    return
}
// Correctly perform elegant shutdown of the server("Server shutdown gracefully")

🌲 Release other resources

In addition to explicitly releasing resources, it is necessary for main goroutine to notify other goroutine processes to exit soon and do necessary processing.

For example, after starting our service, we will register with the service center, and then report its own status asynchronously.

In order for the registration center to realize that the service has been offline as soon as possible, it is necessary to actively cancel the service. Before canceling the service, you need to pause asynchronous timed reporting first

Let us do this easily

ctx, cancel := (())
defer func() {
    cancel()
}()
// You need to register at the registration center after the service is startedgo func() {
    tc := (5 * )
    for {
        select {
            case &lt;-:
            // Report status            ("status update success")
            case &lt;-():
            // server closed, return
            ()
            ("stop update success")
            return
        }
    }
}()

There is a more complex utilization in the sample repositoryExample of exiting a child goroutine

🌲 Full picture

Combining all the details above, an elegantly closed http server code is as follows

func registerService(ctx ) {
 tc := (5 * )
 for {
  select {
  case &lt;-:
   // Report status   ("status update success")
  case &lt;-():
   ()
   ("stop update success")
   return
  }
 }
}
func destroyService() {
 ("destroy success")
}
func gracefulShutdown() {
 mainCtx, mainCancel := (())
 // Use ctx to initialize resources, mysql, redis, etc. // ...
 defer func() {
  mainCancel()
  // Actively cancel the service  destroyService()
  // Clean up resources, mysql, redis, etc.  // ...
 }()
 mx := ()
 ("/foo", func(w , r *) {
  (((10)) * )
  ([]byte("Receive path foo\n"))
 })
 srv := {
  Addr:    ":8009",
  Handler: mx,
 }
 // ListenAndServe will also block, and you need to put it in a goroutine go func() {
  if err := (); err != nil &amp;&amp; err !=  {
   panic(err)
  }
 }()
 // You need to register at the registration center after the service is started go registerService(mainCtx)
 signalCh := make(chan , 1)
 (signalCh, , )
 // Wait for signal sig := &lt;-signalCh
 ("Received signal: %v\n", sig)
 ctxTimeout, cancelTimeout := ((), 5*)
 defer cancelTimeout()
 if err := (ctxTimeout); err != nil {
  select {
  case &lt;-():
   // Since the timeout time is reached, the server is closed, the elegant shutdown is not completed   ("timeout of 5 seconds.")
  default:
   // Service shutdown exception caused by other reasons, elegant shutdown is not completed   ("Server shutdown failed: %v\n", err)
  }
  return
 }
 // Correctly perform elegant shutdown of the server ("Server shutdown gracefully")
}

The above is the detailed content of the elegant closure of the Go standard library http server. For more information about the closure of the Go standard library http server, please pay attention to my other related articles!