A Server in Ten Lines
A working web server in ten lines: net/http attaches a function to a path, and ListenAndServe answers requests on a port.
http.ListenAndServe(addr string, handler http.Handler) errorA web server is a program that waits for requests and sends back replies. A request arrives over the network, the server answers it, and the connection closes or is reused. Go has this built in, in a package called net/http. Nothing needs to be installed, and for many services nothing else is needed.
Two pieces do the work. http.HandleFunc says which function answers which path. A path is the part of the address after the host name, such as /about. http.ListenAndServe opens a port and starts accepting requests. A port is a numbered door on the machine, and ":8080" means port 8080 on every address this machine has.
What the handler function receives
The function is given two values. The first, usually named w, is where the reply is written. Writing bytes to it sends them to the client. The second, r, holds what was asked for: the path, the method such as GET or POST, the headers, and the body. The function returns nothing; the reply is the writing it did.
What trips people up
ListenAndServe blocks. It does not return while the server is healthy, so any code written after it never runs. It returns an error only when the server stops, which is why it is usually wrapped in log.Fatal to print that error and exit. Passing nil as the handler uses a router that the whole program shares.
Example
package main
import (
"fmt"
"log"
"net/http"
)
// greet answers one request. w is where the reply is written.
// r holds what the client asked for.
func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, world")
}
func main() {
http.HandleFunc("/", greet) // every path goes to greet
log.Println("listening on :8080")
// ListenAndServe blocks. It returns only if the server fails.
log.Fatal(http.ListenAndServe(":8080", nil))
}
// Run it with: go run .
// Then, in a second terminal: curl http://localhost:8080/
// curl prints:
// hello, worldWhen to use it
- Expose a health endpoint from a program that already does other work.
- Stand up a stub service so a frontend has something to call during development.
- Receive a webhook from a payment provider or a CI system.
More examples
Header, status, body: that order
Headers and the status line go out before the body, so they can only be set before the first write. Reach for this shape whenever the reply is anything other than a 200 with plain text: a created resource, a redirect, an error. http.Error does all three steps for you in one call.
func createUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
// Headers first, then the status code, then the body.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) // 201
fmt.Fprint(w, `{"id":1}`)
}Your own server value, with timeouts
http.ListenAndServe is shorthand that builds an http.Server with only Addr and Handler set, which means no timeouts at all: one slow client can hold a connection open indefinitely. Any server you intend to deploy builds the value itself so the timeouts are set. The two forms are otherwise identical.
srv := &http.Server{
Addr: ":8080",
Handler: mux, // the router this server uses
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(srv.ListenAndServe())One path, 404 for everything else
The pattern "/" matches every path, so a single handler has to sort requests out by hand. This is worth writing once to see why routers exist; it is exactly the work http.ServeMux does for you in the next lessons.
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
http.NotFound(w, r) // 404 with a plain-text body
return
}
fmt.Fprintln(w, "ok")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Discussion