servidor-http-basico-go-net-http

Basic HTTP Server in Go with net/http, Routes, and JSON

  • 3 min

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))
}
Copied!

Run it:

go run main.go
Copied!

And open in the browser:

http://localhost:8080
Copied!

http.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))
}
Copied!

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)
}
Copied!

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))
}
Copied!

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)
})
Copied!

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)
    }
}
Copied!

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))
}
Copied!

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)
    }
}
Copied!

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.