Language Internals
Part 10 of 11 · Go Language ProficiencyStdlib — HTTP, io/fs, JSON & CLI
Stdlib HTTP, io and fs, JSON including json/v2, and CLI flags.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
How do you route GET /users/id on a current ServeMux?
Answer
HandleFunc with the pattern GET /users/{id}. PathValue reads id. The pattern style has been current since Go 1.22.
L2
Which context does the handler use?
Answer
r.Context. It cancels when the client leaves or the server shuts down.
L3
Which timeout is the minimum on http.Server?
Answer
ReadHeaderTimeout, plus read and write timeouts that fit the route. ListenAndServe on a bare handler sets none of them.
L4
How do you stop accepting and still finish in-flight requests?
Answer
Listen for the signal and call Server.Shutdown with a timeout context. Close drops those requests.
L5
What does a json struct tag do?
Answer
It names the wire field. omitempty drops the zero value. The Go field name stays exported.
L6
What is encoding/json/v2 for on Go 1.27?
Answer
A new JSON API with stricter defaults. Classic encoding/json remains supported.
L7
When is flag enough?
Answer
A few options on a small CLI. Larger command trees can move to another library later. embed still ships the files.
Failure modes
No server timeouts
A slow client holds a goroutine because ReadHeaderTimeout was never set.
Shutdown via Close
In-flight requests die instead of draining.
Ignoring r.Context
The handler keeps working after the client is gone.
Public pprof on the same mux
Debug routes ship on the service port. Bind profiles carefully. That checklist is the next page.
Misconceptions
You need a framework to read a path parameter.
ServeMux patterns and PathValue have been in the stdlib since Go 1.22.
encoding/json/v2 deleted encoding/json.
Classic json stays. v2 is the API to evaluate for new Go 1.27 services.
json.Marshal and Encoder are identical in cost.
Use json.Encoder to write straight to the ResponseWriter (it still buffers each value).
embed is a build tool you install.
The go:embed directive is part of the toolchain. fs.ReadFile reads the embedded tree.
Interviewer traps
Calling ListenAndServe and skipping Shutdown.
Name Server.Shutdown and the timeout context.
Putting the id only in a global.
Read PathValue and pass r.Context into the lookup.
Saying omitempty hides every empty struct.
It skips the zero value of that field. Say zero value, not a TypeScript optional mark.
Design scenario
Same prompt for every reader.
Requirements
ServeMux pattern, PathValue, ReadHeaderTimeout, json Encoder or json/v2, Shutdown.
Traffic / scale
Short JSON responses, some slow clients.
Latency
Header timeout protects the accept loop. Shutdown bounds the drain.
Consistency
A canceled request does not keep writing after the client leaves.
Availability
Close during deploy drops requests that Shutdown would have finished.
Failure assumptions
- The framework sets timeouts and nobody looks.
- json/v2 is required and classic json no longer compiles.
Constraints
- Stay on the stdlib plus the 1.27 JSON note.
- Do not design a gateway.
Prompt
Serve GET /users/id as JSON from the stdlib, with timeouts and a drain on SIGTERM.
Stdlib server versus a framework default
Prefer
Visible timeouts
The handler, the encoder, and the shutdown call are in your file.
- PathValue reads the id.
- Context comes from the request.
- Shutdown drains.
Alternative
Framework defaults
Express and FastAPI are shorter. The timeout is whichever default the version shipped.
- Routing is a helper.
- JSON is return-value magic.
- You still need a drain on deploy.
Overview
net/http, encoding/json, io/fs, embed, and flag cover a small service and a small CLI. On Go 1.27.1, evaluate encoding/json/v2 for new JSON work. Classic encoding/json remains supported. The stdlib also includes a uuid package in that release.
Decisions
- 1
Step 1 http.Server with read, write, and idle timeouts
- nextStep 2 ServeMux pattern GET /users/{id}
- zero timeoutsFailure path - slow clients hold connections open
- 2
Step 2 ServeMux pattern GET /users/{id}
- nextStep 3 Handler reads r.PathValue and r.Context
- 3
Step 3 Handler reads r.PathValue and r.Context
- nextStep 4 Decode the JSON body behind http.MaxBytesReader
- 4
Step 4 Decode the JSON body behind http.MaxBytesReader
- nextStep 5 Input valid
- ?
Step 5 Input valid
- yesStep 6a Encode the JSON response
- noStep 6b http.Error with 400
- 6
Step 6a Encode the JSON response
- nextStep 7 srv.Shutdown(ctx) drains on SIGTERM
- 7
Step 6b http.Error with 400
- 8
Step 7 srv.Shutdown(ctx) drains on SIGTERM
- 9
Failure path - slow clients hold connections open
Lesson map
Stdlib — HTTP, io/fs, JSON & CLI
Stdlib HTTP, io and fs, JSON including json/v2, and CLI flags.
Architecture. Step 1 http.Server with read, write, and idle timeouts Ready. Step 2 ServeMux pattern GET /users/{id} Ready. Step 3 Handler reads r.PathValue and r.Context Ready. Step 4 Decode the JSON body behind http.MaxBytesReader Ready. Step 5 Input valid Ready. Step 6a Encode the JSON response Ready. Step 6b http.Error with 400 Ready. Step 7 srv.Shutdown(ctx) drains on SIGTERM Ready. Failure path - slow clients hold connections open Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB
A["Step 1 http.Server with read, write, and idle timeouts Ready"]
B["Step 2 ServeMux pattern GET /users/{id} Ready"]
C["Step 3 Handler reads r.PathValue and r.Context Ready"]
D["Step 4 Decode the JSON body behind http.MaxBytesReader Ready"]
E["Step 5 Input valid Ready"]
F["Step 6a Encode the JSON response Ready"]
G["Step 6b http.Error with 400 Ready"]
H["Step 7 srv.Shutdown(ctx) drains on SIGTERM Ready"]
X["Failure path - slow clients hold connections open Ready"]
A -->|continues| B
B -->|continues| C
C -->|continues| D
D -->|continues| E
E -->|yes| F
E -->|no| G
F -->|continues| H
A -->|zero timeouts| XPress Run. Snippets must be self-contained — no network, files, or native modules.
The Python object becomes one JSON object. A Go struct does the same through tags and an encoder.
Rosetta — route and JSON
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_ = r.Context()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"id": id})
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}import express from "express";
const app = express();
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id });
});
app.listen(8080);from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{id}")
def get_user(id: str):
return {"id": id}GET /users/{id} is a method pattern. PathValue reads the wildcard. Pass r.Context() into the database call. Use json.Encoder to write straight to the ResponseWriter (it still buffers each value). New Go 1.27 code can look at encoding/json/v2 for stricter defaults. Do not treat v2 as a silent rewrite of every existing Marshal call. Set ReadHeaderTimeout. A bare ListenAndServe does not.
Rosetta — flags
package main
import (
"flag"
"fmt"
)
func main() {
addr := flag.String("addr", ":8080", "listen address")
flag.Parse()
fmt.Println(*addr)
}import { parseArgs } from "node:util";
const { values } = parseArgs({
options: { addr: { type: "string", default: ":8080" } },
});
console.log(values.addr);import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--addr", default=":8080")
print(parser.parse_args().addr)flag covers a small CLI the way node:util parseArgs and argparse do. A larger command tree can wait.
embed and struct tags
package static
import (
"embed"
"io/fs"
)
//go:embed static/*
var staticFS embed.FS
func ReadConfig() ([]byte, error) {
return fs.ReadFile(staticFS, "static/config.json")
}import { readFile } from "node:fs/promises";
export async function readConfig() {
return readFile(new URL("./static/config.json", import.meta.url));
}from importlib.resources import files
def read_config() -> bytes:
return files("mypkg").joinpath("static/config.json").read_bytes()go:embed copies the files into the binary. TypeScript and Python usually read from disk or package data.
type User struct {
ID string `json:"id"`
Email string `json:"email,omitempty"`
}ID is the Go name. id is the JSON name. omitempty skips the empty string.
Graceful shutdown listens for the process signal and calls srv.Shutdown(ctx) with a timeout. In-flight handlers finish. Close does not give them that chance. Go 1.27.1 does not change Shutdown. It does give you encoding/json/v2 and stdlib uuid when you are ready to adopt them on purpose.
Interview Q&A
How do you shut down gracefully?
Answer
Catch the signal and call Server.Shutdown with a timeout context so in-flight requests can finish.
Why ReadHeaderTimeout?
Answer
A client that never sends headers would hold the connection. The timeout bounds that wait.
Where is the request cancel signal?
Answer
r.Context. Pass it to every outbound call the handler makes.
Classic json or json/v2?
Answer
Classic encoding/json still works on Go 1.27.1. Evaluate encoding/json/v2 for new services and stricter defaults.
What does omitempty skip?
Answer
The zero value of the field, such as an empty string. It does not mean the key is optional in the sense of a TypeScript question mark.
Is embed a separate tool?
Answer
No. The go:embed comment is a compiler directive. io/fs reads the resulting FS.
When do you leave flag?
Answer
When subcommands and help text outgrow a flat set of options. The language does not require that library on day one.
What does PathValue require?
Answer
A pattern that declared the wildcard, such as GET /users/{id}, on the ServeMux registered with the server.
Pitfalls
http.ListenAndServewith no timeouts.Closeon deploy.- Decoding JSON into a map when a struct tag would name the field.
- Assuming json/v2 removed the classic package.
A process receives SIGTERM. Say which method you call, what context you pass, and which in-flight work must already be using r.Context.