Tweaking / configuring the HTTP server
The type that implements the HTTP server is http.Server
. If you don't create an http.Server
yourself e.g. because you call the http.ListenAndServe()
function, that creates an http.Server
under the hood for you:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
So if you want to tweek / customize the HTTP server, then create one yourself and call its Server.ListenAndServe()
method yourself. http.Server
is a struct, its zero value is a valid configuration. See its doc what fields it has and so what you can tweak / configure.
The "Process Management" of the HTTP server is documented at Server.Serve()
:
Serve accepts incoming connections on the Listener l, creating a new service goroutine for each. The service goroutines read requests and then call srv.Handler to reply to them. Serve always returns a non-nil error.
So each incoming HTTP request is handled in its new goroutine, meaning they are served concurrently. Unfortunately the API does not document any way to jump in and change how this works.
And looking at the current implementation (Go 1.6.2), there is also no undocumented way to do that. server.go
, currently line #2107-2139:
2107 func (srv *Server) Serve(l net.Listener) error {
2108 defer l.Close()
2109 if fn := testHookServerServe; fn != nil {
2110 fn(srv, l)
2111 }
2112 var tempDelay time.Duration // how long to sleep on accept failure
2113 if err := srv.setupHTTP2(); err != nil {
2114 return err
2115 }
2116 for {
2117 rw, e := l.Accept()
2118 if e != nil {
2119 if ne, ok := e.(net.Error); ok && ne.Temporary() {
2120 if tempDelay == 0 {
2121 tempDelay = 5 * time.Millisecond
2122 } else {
2123 tempDelay *= 2
2124 }
2125 if max := 1 * time.Second; tempDelay > max {
2126 tempDelay = max
2127 }
2128 srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
2129 time.Sleep(tempDelay)
2130 continue
2131 }
2132 return e
2133 }
2134 tempDelay = 0
2135 c := srv.newConn(rw)
2136 c.setState(c.rwc, StateNew) // before Serve can return
2137 go c.serve()
2138 }
2139 }
As you can see in line #2137, the connection is served unconditionally on a new goroutine, so there's nothing you can do about that.
Limiting the "worker" goroutines
If you want to limit the number of goroutines serving requests, you can still do it.
You may limit them on multiple levels. For limiting on the listener level, see Darigaaz's answer. To limit on the Handler level, read on.
For example you could insert a code to each of your http.Handler
or handler functions (http.HandlerFunc
) which only proceeds if the number of concurrent request serving goroutines are less than a specified limit.
There are numerous constructs to such limiting-synchronization code. One example could be: create a buffered channel with capacity being your desired limit. Each handler should first send a value on this channel, and then do the work. When the handler returns, it must receive a value from the channel: so it's best done in a deferred function (not to forget to "clean" itself).
If the buffer is full, a new request attempting to send on the channel will block: wait until a request finishes its work.
Note that you don't have to inject this limiting code to all your handlers, you may use a "middleware" pattern, a new handler type which wraps your handlers, does this limiting-synchronization job, and calls the wrapped handler in the middle of it.
The advantage of limiting in the handler (as opposed to limiting in Listeners) is that in the handler we know what the handler does, so we can do selective limiting (e.g. we may choose to limit certain requests such as database operations, and not to limit others like serving static resources) or we can create multiple, distinct limit groups arbitrarily to our needs (e.g. limit concurrent db requests to 10 max, limit static requests to 100 max, limit heavy computational requests to 3 max) etc. We can also easily realize limitations like unlimited (or high limit) for logged-in / paying users, and low limit for anonymous / non-paying users.
Also note that you can even do the rate-limiting in a single place, without using middlewares. Create a "main handler", and pass that to http.ListenAndServe()
(or Server.ListenAndServe()
). In this main handler do the rate limiting (e.g. using a buffered channel as mentioned above), and simply forward the call to the http.ServeMux
you're using.
Here's a simple example which uses http.ListenAndServe()
and the default multiplexer of the http
package (http.DefaultServeMux
) for demonstration. It limits concurrent requests to 2:
func fooHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Foo called...")
time.Sleep(3 * time.Second)
w.Write([]byte("I'm Foo"))
log.Println("Foo ended.")
}
func barHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Bar called...")
time.Sleep(3 * time.Second)
w.Write([]byte("I'm Bar"))
log.Println("Bar ended.")
}
var ch = make(chan struct{}, 2) // 2 concurrent requests
func mainHandler(w http.ResponseWriter, r *http.Request) {
ch <- struct{}{}
defer func() {
<-ch
}()
http.DefaultServeMux.ServeHTTP(w, r)
}
func main() {
http.HandleFunc("/foo", fooHandler)
http.HandleFunc("/bar", barHandler)
panic(http.ListenAndServe(":8080", http.HandlerFunc(mainHandler)))
}
Deployment
Web applications written in Go do not require external servers to control processes, as the Go webserver itself handles requests concurrently.
So you may start your webserver written in Go as-is: the Go webserver is production ready.
You may of course use other servers for additional tasks if you wish so (e.g. HTTPS handling, authentication / authorization, routing, load balancing between multiple servers).
http.Server
(read its doc for details). The package doc ofnet/http
contains an example of this. Also, incoming HTTP requests are handled concurrently, by creating a new service goroutine for each, as documented atServer.Serve()
. – Amabil