Go, chapter by chapter · Lesson 9 of 62 · about 50 min
Servers, flags & your own packages
Build TCP and HTTP servers with the standard library, read command-line flags, and split your code into documented packages others can import.
On this page · 10 sections
TCP: the raw connection#
TCP gives you a reliable two-way byte stream between two programs. A server does three things:
- Listen on an address:
ln, err := net.Listen("tcp", ":9000"). - Accept connections in a loop:
conn, err := ln.Accept()blocks until a client connects. - Handle each connection in its own goroutine, so other clients are not kept waiting.
A net.Conn is both an io.Reader and an io.Writer, so everything from the last lesson works on it. An echo server is literally io.Copy(conn, conn). Clients connect with net.Dial("tcp", "host:9000").
HTTP: the web#
HTTP is built on top of TCP. In Go you register handlers, functions that receive a request and write a response:
mux := http.NewServeMux()
mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "halo, %s\n", r.PathValue("name"))
})
http.ListenAndServe(":8080", mux)wis anio.Writer: anything you write becomes the response body.rholds the method, URL, headers and body (anio.Reader).- Since Go 1.22, patterns can include a method (
GET,POST) and wildcards like{name}, read withr.PathValue. http.Error(w, msg, code)sends an error status.encoding/jsonturns structs into JSON and back:json.NewDecoder(r.Body).Decode(&v)andjson.NewEncoder(w).Encode(v).- Each request runs in its own goroutine automatically.
For tests, net/http/httptest starts a real server on a random port, or records a response without any network.
Command-line flags#
port := flag.Int("port", 8080, "HTTP port")
verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
fmt.Println(*port, *verbose, flag.Args()) // Args: what's left after the flagsThe functions return pointers because values are filled in only when flag.Parse() reads os.Args. Running the program with -h prints usage automatically.
Creating your own packages#
A package is a folder of .go files sharing one package name. Inside a module:
godsa/ module github.com/you/godsa
├── go.mod
├── stats/
│ ├── stats.go package stats
│ └── stats_test.go
└── cmd/server/
└── main.go package main, imports ".../godsa/stats"- Import by module path plus folder:
import "github.com/you/godsa/stats". - Only capitalized names are visible outside the package. Lowercase helpers stay private.
- A folder named
internal/can only be imported by code in its parent tree: private by design. - Keep
mainsmall: parse flags, wire things together, start the server. Logic goes in packages you can test.
Documentation#
Go documentation is just comments, written right above the declaration and starting with its name:
// Package stats provides small numeric helpers.
package stats
// Median returns the middle value of xs, or 0 for an empty slice.
// xs is not modified.
func Median(xs []float64) float64 { ... }Read it with go doc github.com/you/godsa/stats (or go doc ./stats). Once the module is public on GitHub, pkg.go.dev renders it automatically. Example functions in test files appear in those docs as runnable examples.
More examples#
RPC: calling a function in another process
The book's last server example uses net/rpc: a client calls a method on a server as if it were local. The method must have the shape func (t *T) Method(args A, reply *R) error. The program below connects the client and the server through net.Pipe, an in-memory connection, so it runs anywhere; over a network you would use rpc.Dial("tcp", addr).
package main
import (
"fmt"
"net"
"net/rpc"
)
type Server struct{}
// Negate is callable remotely: exported, two arguments, the second a pointer, returning error.
func (s *Server) Negate(i int64, reply *int64) error {
*reply = -i
return nil
}
type Pair struct{ A, B int }
func (s *Server) Div(p Pair, reply *int) error {
if p.B == 0 {
return fmt.Errorf("division by zero")
}
*reply = p.A / p.B
return nil
}
func main() {
srv := rpc.NewServer()
if err := srv.Register(new(Server)); err != nil {
panic(err)
}
serverConn, clientConn := net.Pipe()
go srv.ServeConn(serverConn)
client := rpc.NewClient(clientConn)
defer client.Close()
var neg int64
err := client.Call("Server.Negate", int64(999), &neg)
fmt.Println(neg, err)
var q int
err = client.Call("Server.Div", Pair{10, 0}, &q)
fmt.Println("remote error:", err)
}-999 <nil>
remote error: division by zeronet/rpc is frozen: it still works, but new systems use gRPC (with Protocol Buffers) or plain HTTP with JSON, which every language can call. The idea is the same: serialize the arguments, send them, run the function remotely, send back the result or the error.
A production-shaped HTTP server
The servers in this lesson keep things minimal. Real services add three things: timeouts so slow clients cannot hold connections forever, middleware for logging and recovery, and graceful shutdown so in-flight requests finish when you deploy a new version. This program is marked to be built, not run, because it serves until you press Ctrl+C:
package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// logging is middleware: it wraps a handler and runs code around every request.
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path, "took", time.Since(start))
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
srv := &http.Server{
Addr: ":8080",
Handler: logging(mux),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
slog.Info("listening", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != nilerrors.Is(err, http.ErrServerClosed) {
slog.Error("server failed", "err", err)
os.Exit(1)
}
}()
// Wait for Ctrl+C or a termination signal from the platform.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
// Give in-flight requests up to 10 seconds to finish.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("shutdown", "err", err)
}
slog.Info("stopped cleanly")
}Run it with go run ., open http://localhost:8080/healthz, and watch the structured log/slog lines. You will reuse this skeleton for every project in the system design module.
The full program#
This program runs everything in one process so you can see the full round trip: it starts a TCP echo server and an HTTP API on random free ports, calls both as a client, and prints the replies.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"slices"
"strings"
)
// Median returns the middle value of xs, or 0 for an empty slice.
// In a real project this lives in package stats.
func Median(xs []float64) float64 {
if len(xs) == 0 {
return 0
}
s := slices.Sorted(slices.Values(xs))
m := len(s) / 2
if len(s)%2 == 0 {
return (s[m-1] + s[m]) / 2
}
return s[m]
}
func newMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "halo, %s\n", r.PathValue("name"))
})
mux.HandleFunc("POST /median", func(w http.ResponseWriter, r *http.Request) {
var xs []float64
if err := json.NewDecoder(r.Body).Decode(&xs); err != nil {
http.Error(w, "send a JSON array of numbers", http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(map[string]float64{"median": Median(xs)})
})
return mux
}
func serveEcho(ln net.Listener) {
for {
conn, err := ln.Accept()
if err != nil {
return // listener closed
}
go func() {
defer conn.Close()
io.Copy(conn, conn)
}()
}
}
func main() {
verbose := flag.Bool("v", false, "print extra detail")
flag.Parse()
// TCP
ln, err := net.Listen("tcp", "127.0.0.1:0") // port 0: pick any free port
if err != nil {
fmt.Println(err)
return
}
defer ln.Close()
go serveEcho(ln)
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(conn, "ping")
reply, _ := bufio.NewReader(conn).ReadString('\n')
conn.Close()
fmt.Print("tcp echo: ", reply)
// HTTP
srv := httptest.NewServer(newMux())
defer srv.Close()
res, err := http.Get(srv.URL + "/hello/davi")
if err != nil {
fmt.Println(err)
return
}
body, _ := io.ReadAll(res.Body)
res.Body.Close()
fmt.Print("GET: ", string(body))
res, err = http.Post(srv.URL+"/median", "application/json", strings.NewReader("[3, 1, 4, 1, 5]"))
if err != nil {
fmt.Println(err)
return
}
body, _ = io.ReadAll(res.Body)
res.Body.Close()
fmt.Print("POST: ", res.StatusCode, " ", string(body))
res, _ = http.Post(srv.URL+"/median", "application/json", strings.NewReader("oops"))
fmt.Println("bad input:", res.StatusCode)
res.Body.Close()
if *verbose {
fmt.Println("server was at", srv.URL)
}
}tcp echo: ping
GET: halo, davi
POST: 200 {"median":3}
bad input: 400To run it as a real long-lived server instead, replace the httptest.NewServer part with http.ListenAndServe(":8080", newMux()), start it, and in another terminal run curl localhost:8080/hello/davi.
Summary#
- TCP:
net.Listen, anAcceptloop, one goroutine per connection;net.Connis a Reader and Writer. - HTTP:
http.NewServeMuxwith method and wildcard patterns,encoding/jsonfor bodies,httptestfor tests. flagparses command-line options into pointers afterflag.Parse().- One folder is one package; capitalized names are exported;
internal/hides code. - Doc comments go directly above declarations, start with the name, and appear in
go docand pkg.go.dev.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.Which identifiers in package
statscan another package use?Q2.Why does a TCP server start a goroutine for each accepted connection?
Q3.What does
flag.Int("port", 8080, "HTTP port")return?Q4.Where should a doc comment for function
Mediango?
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
- TCP echo clientbuild
Write a client that connects with
net.Dial, sends three lines, and prints each echoed reply.Show hint
Wrap the connection in
bufio.NewReader(conn)and callReadString('\n')after each write. - Two Sum as a servicebuild
Add
POST /twosumtaking{"nums": [...], "target": n}and returning the two indexes. Test it withhttptest.Show hint
Decode into a struct with tags like
json:"nums". - Document and publish a packagebuild
Move
Medianintogithub.com/you/godsa/statswith doc comments and anExampleMedianfunction. View it withgo doc ./stats. - Word count with flagsbuild
Turn last lesson's word counter into
wordfreq -top 5 file.txtusingflag.Intandflag.Args().
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.