Skip to content
Muhammet Şafak
tr
Languages 3 min read

Writing HTTP services in Go: is the standard library enough?

I explore what you get — and what you don't — when building a small HTTP service with Go's net/http package, and where the framework threshold sits.


One rule I set for myself while learning Go was this: I wouldn’t pull in an external library until I’d tried to build the thing with the standard library first. That rule was occasionally frustrating, but it taught me a lot. HTTP services turned out to be the area that really put it to the test.

In PHP, some framework is always running underneath everything — Laravel, Slim, whatever. Writing an HTTP server in raw PHP is theoretically possible, but nobody does it. In Go, on the other hand, the net/http package is genuinely usable; production services get written with the standard library every day. That difference forces a shift in mindset.

A basic server with net/http

Getting a Go HTTP server running takes just a few lines:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintln(w, `{"status":"ok"}`)
    })

    http.ListenAndServe(":8080", nil)
}

It runs, responds on port 8080, zero dependencies. That simplicity feels satisfying at first — but as soon as you start building a real API, the limitations surface.

What the standard library is missing

The HandleFunc-based registration mechanism in net/http isn’t flexible enough. Route parameters — dynamic path segments like /users/123 — don’t exist in the standard library. You have to parse them by hand:

http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
    // manually extract everything after "/users/"
    id := r.URL.Path[len("/users/"):]
    if id == "" {
        // return list
        return
    }
    // return single record
})

This approach is tolerable for two or three routes, but it becomes unmanageable at ten. HTTP method dispatch (GET/POST/PUT/DELETE) also has to be done by hand:

switch r.Method {
case http.MethodGet:
    // handle GET
case http.MethodPost:
    // handle POST
default:
    http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}

Copy-pasting that switch block into every handler inflates the code quickly. The standard library doesn’t abstract this common pattern away — you either write it yourself or delegate it to a library.

Where is the framework threshold?

Before reaching for a library, I ask myself: how generic is the need that the standard library isn’t covering? Route parameters and HTTP method routing are generic and universal — every service needs them.

That’s when I look at third-party packages. In the Go ecosystem, gorilla/mux and chi are lightweight routers that fill this gap. They’re not frameworks; they simply add route parameters and method matching on top of net/http, and they conform to the standard http.Handler interface.

import "github.com/go-chi/chi/v5"

r := chi.NewRouter()

r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) {
    id := chi.URLParam(req, "id")
    // work with id
})

http.ListenAndServe(":8080", r)

The core philosophy is preserved: a plugin that conforms to the standard library’s interfaces and doesn’t conflict with other packages. Not a large dependency pile like Laravel.

What convinced me about chi is its full compliance with the standard http.Handler interface. When evaluating a library, I pay close attention to this; a router that breaks the standard interface makes it harder to work with middleware or utilities that expect it. A handler written with chi can be used with any other net/http-compatible setup.

JSON responses and error handling

The standard library ships encoding/json for serialization, and it works well:

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

This small helper eliminates repetition; all handlers use it.

Comparison with PHP

Writing an HTTP service in Go is different from writing one in PHP without a framework: the standard library is genuinely production-ready, well-maintained, and fast. But the layers Laravel provides — authentication, ORM, queues, email — aren’t present in Go, nor do they need to be. Go services typically do one narrower responsibility very well; broader concerns bring in other languages.


The bottom line: Go’s standard library covers a great deal, but not everything. Reaching for a lightweight library for route parameters and method routing is a reasonable trade-off. Jumping to a full framework is a premature decision in most cases — something you can do when the need genuinely grows to that scale.

Tags: #Go
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind