Skip to content

Table of Contents

Standard Library Essentials

Contents

net/http server

import "net/http"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /users/{id}", getUser) // method + path wildcards (Go 1.22+)
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        io.WriteString(w, "ok")
    })

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")                  // Go 1.22+ path wildcard
    q := r.URL.Query().Get("verbose")        // ?verbose=...
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"id": id})
}

// Middleware = func(http.Handler) http.Handler
func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
    })
}
// srv.Handler = logging(mux)

Graceful shutdown:

go srv.ListenAndServe()
<-ctx.Done()                                 // e.g. on SIGINT
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(shutCtx)

net/http client

Don't use http.Get for production — create a client with timeouts and always close the body.

client := &http.Client{Timeout: 10 * time.Second}

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req.Header.Set("Authorization", "Bearer "+token)

resp, err := client.Do(req)
if err != nil { return err }
defer resp.Body.Close()                      // ALWAYS, even on non-2xx

if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("status %d", resp.StatusCode)
}
var out Result
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
    return err
}

// POST JSON:
body, _ := json.Marshal(payload)
resp, err = client.Post(url, "application/json", bytes.NewReader(body))

encoding/json

import "encoding/json"

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`    // omit when zero
    pass  string `json:"-"`                   // unexported never marshaled anyway
}

// Marshal / Unmarshal (whole value in memory)
b, err := json.Marshal(u)                    // []byte
b, _ = json.MarshalIndent(u, "", "  ")       // pretty
err = json.Unmarshal(b, &u)                  // pass a pointer

// Streaming (preferred for I/O)
json.NewEncoder(w).Encode(u)                 // adds trailing newline
json.NewDecoder(r.Body).Decode(&u)

// Arbitrary / unknown shape
var data map[string]any
json.Unmarshal(b, &data)                     // numbers decode to float64!

// Decoder options
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()                  // strict
dec.UseNumber()                              // numbers as json.Number, not float64

// Custom marshaling
func (u User) MarshalJSON() ([]byte, error) { ... }
func (u *User) UnmarshalJSON(b []byte) error { ... }

// RawMessage delays parsing of a sub-field
type Envelope struct {
    Type string          `json:"type"`
    Data json.RawMessage `json:"data"`
}

Gotchas: only exported fields marshal; any numbers become float64; nil slice -> null, empty slice -> []; time.Time marshals as RFC3339.

database/sql

import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib"       // driver, blank import registers it
)

db, err := sql.Open("pgx", dsn)              // doesn't connect yet
if err != nil { log.Fatal(err) }
defer db.Close()
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
if err := db.PingContext(ctx); err != nil {  // actually connects
    log.Fatal(err)
}

// Query a single row
var name string
err = db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&name)
if errors.Is(err, sql.ErrNoRows) { /* not found */ }

// Query many rows
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil { return err }
defer rows.Close()
for rows.Next() {
    var u User
    if err := rows.Scan(&u.ID, &u.Name); err != nil { return err }
    users = append(users, u)
}
if err := rows.Err(); err != nil { return err } // check iteration error!

// Exec (INSERT/UPDATE/DELETE)
res, err := db.ExecContext(ctx, "UPDATE users SET name=$1 WHERE id=$2", name, id)
n, _ := res.RowsAffected()

// Transaction
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()                          // no-op if already committed
if _, err := tx.ExecContext(ctx, "..."); err != nil { return err }
return tx.Commit()

Always use placeholders ($1/?) — never string-concatenate SQL. Always defer rows.Close() and check rows.Err().

io & bufio

import ("io"; "bufio"; "os")

io.Copy(dst, src)                            // stream copy
io.ReadAll(r)                                // read everything ([]byte)
io.WriteString(w, "text")
io.Discard                                   // /dev/null writer
io.MultiReader(r1, r2); io.MultiWriter(w1, w2)
io.LimitReader(r, 1<<20)                      // cap bytes read
io.TeeReader(r, logWriter)                    // copy reads to a side writer

// Buffered line scanning (default token = line)
f, _ := os.Open("file.txt")
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 1024), 1024*1024)   // raise max line size
for sc.Scan() {
    line := sc.Text()                          // or sc.Bytes()
    _ = line
}
if err := sc.Err(); err != nil { /* handle */ }

sc.Split(bufio.ScanWords)                      // word/rune/byte tokenizers

// Buffered writer — remember to Flush
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
fmt.Fprintln(w, "buffered")

r := bufio.NewReader(conn)
line, err := r.ReadString('\n')

time

import "time"

now := time.Now()
now.Unix()                                   // seconds since epoch
now.UTC(); now.Local()
now.Format("2006-01-02 15:04:05")            // REFERENCE TIME layout (Mon Jan 2 15:04:05 MST 2006)
t, err := time.Parse(time.RFC3339, "2026-06-26T10:00:00Z")
t, err = time.ParseInLocation("2006-01-02", s, loc)

d := 90 * time.Minute
d.Hours()                                    // 1.5
time.Since(start)                            // elapsed (shortcut for Now().Sub)
time.Until(deadline)

now.Add(24 * time.Hour)
now.AddDate(0, 1, 0)                          // +1 month
now.Sub(other)                               // Duration
now.Before(t); now.After(t); now.Equal(t)
now.Truncate(time.Hour); now.Round(time.Minute)

// Timers & tickers — stop them to avoid leaks
timer := time.NewTimer(2 * time.Second)
<-timer.C
timer.Stop()

tick := time.NewTicker(time.Second)
defer tick.Stop()
for range tick.C { doPeriodic() }

<-time.After(time.Second)                     // one-off (used in select)
time.Sleep(100 * time.Millisecond)

The magic layout string is 2006-01-02 15:04:05 (Mon=1, Jan=1, 2pm=15...). Memorize it: 01/02 03:04:05PM '06 -0700.

context

See concurrency.md for full detail.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
row := db.QueryRowContext(ctx, sql, args...)
select {
case <-ctx.Done():
    return ctx.Err()
case r := <-ch:
    return r
}

log/slog

Structured logging, stdlib since Go 1.21.

import "log/slog"

slog.Info("server started", "port", 8080, "env", "prod")
slog.Error("query failed", "err", err, "id", id)
slog.Warn("retrying", slog.Int("attempt", 3))

// JSON handler + level control
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
logger := slog.New(h)
slog.SetDefault(logger)                       // make it the package default

// Contextual child loggers
reqLog := logger.With("request_id", id, "user", uid)
reqLog.Info("handling")

// Groups and typed attrs
logger.Info("io", slog.Group("file", "name", n, "size", sz))

// Context-aware
logger.InfoContext(ctx, "msg")

Old log package still fine for simple cases: log.Printf, log.Fatal (logs then os.Exit(1)), log.Panic.

flag

import "flag"

var (
    port    = flag.Int("port", 8080, "listen port")
    verbose = flag.Bool("v", false, "verbose output")
    name    = flag.String("name", "world", "who to greet")
)

func main() {
    flag.Parse()                              // parse os.Args
    fmt.Println(*port, *verbose, *name)
    args := flag.Args()                       // remaining positional args
    _ = args
}

// Bind into existing variables:
var cfg Config
flag.StringVar(&cfg.Host, "host", "localhost", "host")

// Custom flag set (e.g. for subcommands):
fs := flag.NewFlagSet("serve", flag.ExitOnError)
fs.Int("workers", 4, "worker count")
fs.Parse(os.Args[2:])

Usage: ./app -port 9090 -v file1 file2. -h/--help auto-prints usage.

os/exec

import "os/exec"

// Run and capture combined output
out, err := exec.Command("git", "rev-parse", "HEAD").Output() // stdout only
out, err = exec.Command("ls", "-la").CombinedOutput()         // stdout+stderr

// With context (timeout/cancel) — preferred
cmd := exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run()                               // killed when ctx cancels

// Fine-grained control
cmd = exec.Command("grep", "foo")
cmd.Stdin = strings.NewReader("foo\nbar\n")
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
cmd.Env = append(os.Environ(), "LANG=C")
cmd.Dir = "/tmp"
if err := cmd.Run(); err != nil {
    var ee *exec.ExitError
    if errors.As(err, &ee) {
        fmt.Println("exit code:", ee.ExitCode(), stderr.String())
    }
}

// Streaming with pipes
cmd = exec.Command("long-running")
stdoutPipe, _ := cmd.StdoutPipe()
cmd.Start()
io.Copy(os.Stdout, stdoutPipe)
cmd.Wait()

// Resolve a binary in PATH
path, err := exec.LookPath("docker")

Run = Start + Wait. Output/CombinedOutput capture; raw Command inherits nothing unless you wire Stdout/Stderr.