An HTTP server in Go is a program that listens for web requests and returns responses typically using the standard net/http package.
With the standard library, we can set up a server, register routes, and return text or JSON without installing external dependencies.
Let’s create the smallest possible server and then gradually make it a bit more useful.
The Minimal Server
Let’s start with the basics:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Go")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}Run it:
go run main.goAnd open in the browser:
http://localhost:8080http.HandleFunc registers a function for a route. http.ResponseWriter is used to write the response. *http.Request contains the request information. ListenAndServe starts the server on the specified port.
Named Handlers
For small examples, an anonymous function is fine. As the code grows, it’s better to separate the handlers.
package main
import (
"fmt"
"log"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Home page")
}
func health(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
}
func main() {
http.HandleFunc("/", home)
http.HandleFunc("/health", health)
log.Fatal(http.ListenAndServe(":8080", nil))
}This already looks more like a real application: each route has its own responsibility.
Reading Method and Route
The *http.Request object contains a lot of information about the request.
func info(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Method: %s\n", r.Method)
fmt.Fprintf(w, "Path: %s\n", r.URL.Path)
}Since Go 1.22, ServeMux can include the method in the pattern and automatically returns 405 Method Not Allowed with the Allow header when the route exists for other methods:
func createUser(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "User created")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /users", createUser)
log.Fatal(http.ListenAndServe(":8080", mux))
}When you manually return an error, use return afterwards if you don’t want the handler to continue executing.
Path Parameters
Patterns also support wildcards. Their values are read using PathValue:
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintln(w, "User:", id)
})A / pattern matches any route not captured by another pattern. To represent only the root, we can use GET /{$}.
Returning JSON
A modern server almost always ends up returning JSON.
package main
import (
"encoding/json"
"log"
"net/http"
)
type Response struct {
Message string `json:"message"`
Status string `json:"status"`
}
func api(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := Response{
Message: "Hello from Go",
Status: "ok",
}
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("encoding response: %v", err)
}
}We use json.NewEncoder(w) because ResponseWriter is already a write destination. There’s no need to build the JSON in memory beforehand.
Using a Custom ServeMux
In quick examples, it’s common to use the global mux with http.HandleFunc. For real applications, it’s better to create your own.
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", home)
mux.HandleFunc("GET /api", api)
log.Fatal(http.ListenAndServe(":8080", mux))
}This avoids relying on global state and makes the code easier to test and maintain.
Configuring the Server and Handling Errors
ListenAndServe returns an error. For a real service, it’s also advisable to configure timeouts, especially ReadHeaderTimeout:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", home)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
}
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}http.ErrServerClosed is the normal result when performing a graceful shutdown with Shutdown. Other errors indicate that the server could not start or stopped serving unexpectedly.