-
-
Save peterhellberg/38117e546c217960747aacf689af3dc2 to your computer and use it in GitHub Desktop.
package main | |
import ( | |
"context" | |
"log" | |
"net/http" | |
"os" | |
"os/signal" | |
"time" | |
) | |
type Server struct { | |
logger *log.Logger | |
mux *http.ServeMux | |
} | |
func NewServer(options ...func(*Server)) *Server { | |
s := &Server{ | |
logger: log.New(os.Stdout, "", 0), | |
mux: http.NewServeMux(), | |
} | |
for _, f := range options { | |
f(s) | |
} | |
s.mux.HandleFunc("/", s.index) | |
return s | |
} | |
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
s.mux.ServeHTTP(w, r) | |
} | |
func (s *Server) index(w http.ResponseWriter, r *http.Request) { | |
s.logger.Println("GET /") | |
w.Write([]byte("Hello, World!")) | |
} | |
func main() { | |
stop := make(chan os.Signal, 1) | |
signal.Notify(stop, os.Interrupt) | |
logger := log.New(os.Stdout, "", 0) | |
addr := ":" + os.Getenv("PORT") | |
if addr == ":" { | |
addr = ":2017" | |
} | |
s := NewServer(func(s *Server) { s.logger = logger }) | |
h := &http.Server{Addr: addr, Handler: s} | |
go func() { | |
logger.Printf("Listening on http://0.0.0.0%s\n", addr) | |
if err := h.ListenAndServe(); err != nil { | |
logger.Fatal(err) | |
} | |
}() | |
<-stop | |
logger.Println("\nShutting down the server...") | |
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) | |
h.Shutdown(ctx) | |
logger.Println("Server gracefully stopped") | |
} |
Example with multiple endpoints
EDIT: This example was actually wrong as pointed out by @pci below, I've now corrected it
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
type server struct {
logger *log.Logger
mux *http.ServeMux
}
func newServer(options ...func(*server)) *server {
s := &server{mux: http.NewServeMux()}
for _, f := range options {
f(s)
}
if s.logger == nil {
s.logger = log.New(os.Stdout, "", 0)
}
s.mux.HandleFunc("/", s.index)
s.mux.HandleFunc("/hello/", s.hello)
return s
}
func (s *server) index(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world!"))
}
func (s *server) hello(w http.ResponseWriter, r *http.Request) {
message := "Hello " + strings.TrimPrefix(r.URL.Path, "/hello/")
s.logger.Println(message)
w.Write([]byte(message))
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "example Go server")
s.mux.ServeHTTP(w, r)
}
func main() {
hs, logger := setup()
go func() {
logger.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
logger.Fatal(err)
}
}()
graceful(hs, logger, 5*time.Second)
}
func setup() (*http.Server, *log.Logger) {
addr := ":" + os.Getenv("PORT")
if addr == ":" {
addr = ":2017"
}
logger := log.New(os.Stdout, "", 0)
s := newServer(func(s *server) {
s.logger = logger
})
hs := &http.Server{Addr: addr, Handler: s}
return hs, logger
}
func graceful(hs *http.Server, logger *log.Logger, timeout time.Duration) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
logger.Printf("\nShutdown with timeout: %s\n", timeout)
if err := hs.Shutdown(ctx); err != nil {
logger.Printf("Error: %v\n", err)
} else {
logger.Println("Server stopped")
}
}
Example with separate main
and server
packages
EDIT: This example was actually wrong as pointed out by @pci below, I've now corrected it
main.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"./server" // This import should be changed to something like github.com/<user>/<project>/server
)
func main() {
hs, logger := setup()
go func() {
logger.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
logger.Fatal(err)
}
}()
graceful(hs, logger, 5*time.Second)
}
func setup() (*http.Server, *log.Logger) {
addr := ":" + os.Getenv("PORT")
if addr == ":" {
addr = ":2017"
}
logger := log.New(os.Stdout, "", 0)
return &http.Server{
Addr: addr,
Handler: server.New(server.Logger(logger)),
}, logger
}
func graceful(hs *http.Server, logger *log.Logger, timeout time.Duration) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
logger.Printf("\nShutdown with timeout: %s\n", timeout)
if err := hs.Shutdown(ctx); err != nil {
logger.Printf("Error: %v\n", err)
} else {
logger.Println("Server stopped")
}
}
server/server.go
package server
import (
"log"
"net/http"
"os"
"strings"
)
type Server struct {
logger *log.Logger
mux *http.ServeMux
}
func New(options ...func(*Server)) *Server {
s := &Server{mux: http.NewServeMux()}
for _, f := range options {
f(s)
}
if s.logger == nil {
s.logger = log.New(os.Stdout, "", 0)
}
s.mux.HandleFunc("/", s.index)
s.mux.HandleFunc("/hello/", s.hello)
return s
}
func Logger(logger *log.Logger) func(*Server) {
return func(s *Server) {
s.logger = logger
}
}
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world!"))
}
func (s *Server) hello(w http.ResponseWriter, r *http.Request) {
message := "Hello " + strings.TrimPrefix(r.URL.Path, "/hello/")
s.logger.Println(message)
w.Write([]byte(message))
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "example Go server")
s.mux.ServeHTTP(w, r)
}
$ go run main.go
Listening on http://0.0.0.0:2017
And now you should be able to curl http://0.0.0.0:2017/hello/yourname
Using the github.com/TV4/graceful
package
EDIT: This example is actually wrong as pointed out by @pci below, I've kept it as is in order for his comment to make sense.
You probably want to use
graceful.ListenAndServe
orgraceful.LogListenAndServe
instead ofgraceful.Shutdown
I have written the github.com/TV4/graceful package in order to reduce boilerplate code needed to set up a *http.Server
with graceful shutdown.
package main
import (
"log"
"net/http"
"github.com/TV4/graceful"
)
func main() {
hs := &http.Server{Addr: ":2017", Handler: &server{}}
go graceful.Shutdown(hs)
log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!"))
}
@peterhellberg Hey these are really useful, thanks for putting them together. One thing though, I was testing with some slow requests and it seems like: when server.Shutdown()
is called server.ListenAndServe
returns straight away, even if there are ongoing requests, which then means that the main function exits and the requests are still killed early. In testing a request of a second or two is enough to see the effect.
My workaround was to pass a done channel to graceful
so it can report back once the server has shutdown, but would there be any better ways?
Thanks again for these, in my searching they were definitely the best examples online of how to actually use server.Shutdown
in practice.
@pci Yes, you are absolutely correct. And the example I gave above is actually wrong.
If you block on graceful.Shutdown
instead it seems to work as intended:
package main
import (
"log"
"net/http"
"time"
"github.com/TV4/graceful"
)
func main() {
hs := &http.Server{Addr: ":2017", Handler: &server{}}
go func() {
log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
graceful.Shutdown(hs)
}
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte("Hello!"))
}
(And if you change the sleep from 2 to 20 seconds you'll hit the graceful.DefaultTimout of 15 seconds and get Error: context deadline exceeded
in the log)
@pci I have now simplified it even further by adding a ListenAndServe
function to the graceful
package.
package main
import (
"log"
"net/http"
"time"
"github.com/TV4/graceful"
)
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte("Hello!"))
}
func main() {
addr := ":2017"
log.Printf("Listening on http://0.0.0.0%s\n", addr)
graceful.ListenAndServe(&http.Server{
Addr: addr,
Handler: &server{},
})
}
@pci Since I do logging on the listening URL in almost every service I write I've now also added LogListenAndServe
:
package main
import (
"net/http"
"time"
"github.com/TV4/graceful"
)
type server struct{}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte("Hello!"))
}
func main() {
graceful.LogListenAndServe(&http.Server{
Addr: ":2017",
Handler: &server{},
})
}
$ go run example.go
Listening on http://0.0.0.0:2017
^C
Shutdown with timeout: 15s
Server stopped
@peterhellberg Nice additions 👍
hi~~
The line 62 of first demo "graceful.go" "logger.Fatal(err)" maybe lead to last print (" Server gracefully stopped" )not display
@wangjinbei: Yes, this gist mainly contains improvements to the original code in the comments. I didn't want to change it too much in order to not confuse people :)
@peterhellberg Great examples!
Go vet would indicate a leak on the cancel function for the first one on context.WithTimeout.
I got Error: context deadline exceeded
when I first to request http://0.0.0.0:2017
and then shutdown
in Example with graceful function
There are more below lol. Great job!
hey,
I created a package which does all the work for you with a simple api:
https://github.com/pseidemann/finish
let me know what you think!
@peterhellberg Thanks for posting these examples. Really helpful.
@peterhellberg Thanks for sharing this!
Hi, great contribution.
There is one thing that could make it more complete in my opinion.
Everywhere this code is, it will always print "Listening on http://...." in the console, even if ListenAndServe()
was unable to start actually listening.
log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)
if err := hs.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
If you start 1 server, you'll see this in the console:
Listening on http://0.0.0.0:2017
If you start a 2nd server without shutting down the 1st one, you will see this in the console:
Listening on http://0.0.0.0:2017
panic... listen tcp :2017: bind: address already in use
So it first shows "Listening on http://..." however this never actually happened.
I'm not a go expert, is there a way to implement this?
-- UPDATE
Apparently using http.Listen
and http.Serve
individually, it is possible to catch a port in use error (or any other error while trying to listen to a tcp network) before assuming the server is listening.
Source: https://stackoverflow.com/a/48250354/194630
listener, err := net.Listen("tcp", ":"+config.Port)
if err != nil {
log.Fatal(err)
}
done := make(chan bool)
go server.Serve(listener)
// Log server started
log.Printf("Server started at port %v", config.Port)
<-done
Example with graceful function
EDIT: This example was actually wrong as pointed out by @pci below, I've now corrected it