The standard library toolkit

strings and strconv, time, the slices and maps helpers, encoding/json, flag, log/slog, os and io/fs, and a first net/http server.

Text, numbers and time

import (
    "slices"
    "strconv"
    "strings"
    "time"
)

label := strings.TrimSpace("  Hello,  cafe  ")
parts := strings.Split("a,b,c", ",")
joined := strings.Join(parts, " | ")
has := strings.Contains(strings.ToLower(label), "hello")

n, err := strconv.Atoi("42")              // string -> int
s := strconv.Itoa(7)                      // int -> string
f, _ := strconv.ParseFloat("3.5", 64)
b, _ := strconv.ParseBool("true")

// time: never format with anything but the reference layout
now := time.Now().UTC()
stamp := now.Format(time.RFC3339)         // 2026-09-18T09:30:00Z
t, err := time.Parse(time.RFC3339, stamp)
later := t.Add(90 * time.Minute)
d := later.Sub(t)                          // a Duration, not a timestamp

// the zero-value helpers
nums := []int{3, 1, 2}
slices.Sort(nums)
i, found := slices.BinarySearch(nums, 2)
nums = slices.Compact(slices.Clone([]int{1, 1, 2}))
PackageRemember this
stringsTrim, Split, Join, Cut, HasPrefix, ReplaceAll; everything returns new strings, since strings are immutable
strconvThe string-to-number boundary. Atoi and ParseInt return errors you must check
timeLayouts are reference times, not format strings. Durations are time.Duration; always store UTC and convert at the edge
slices / mapsSort, BinarySearch, Contains, Compact, Clone, Keys, Values — replaces most hand-written loops
encoding/jsonStruct tags drive field names; omitempty and - are the two you will use constantly
log/slogStructured logging with levels and key-value pairs; the default handler is text, NewJSONHandler for production

JSON, files and flags

type Config struct {
    Name  string        `json:"name"`
    Port  int           `json:"port"`
    Debug bool          `json:"debug,omitempty"`
    Key   string        `json:"-"`          // never encoded
}

func load(path string) (Config, error) {
    var c Config
    data, err := os.ReadFile(path)
    if err != nil {
        return c, err
    }
    // Decoder is preferred over Unmarshal: it reports trailing garbage
    if err := json.NewDecoder(bytes.NewReader(data)).Decode(&c); err != nil {
        return c, fmt.Errorf("parse %s: %w", path, err)
    }
    return c, nil
}

func main() {
    verbose := flag.Bool("v", false, "verbose output")
    flag.Parse()                       // flag.Var and flag.Func for custom types

    logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
    logger.Info("starting", "verbose", *verbose, "pid", os.Getpid())

    if err := run(logger); err != nil {
        logger.Error("fatal", "err", err)   // structured, not string concatenation
        os.Exit(1)
    }
}
  • Use json.NewEncoder(w).Encode(v) when the output target is an io.Writer such as a file or response body — it streams instead of building one large buffer.
  • Use json.NewDecoder(r).Decode(&v) on input so trailing data and malformed bodies are detected.
  • Field names must be exported to be encoded. A lowercase field is silently skipped, which is the most common cause of an empty JSON object.
  • Pass the logger as an explicit parameter, or store it in a struct. Package-level loggers make tests noisy and are hard to reconfigure.
  • Use os.ReadFile for small whole files and bufio.Scanner or bufio.Reader for large or line-oriented ones. io/fs also lets you test file code against an in-memory filesystem.

A first HTTP server

package main

import (
    "encoding/json"
    "log/slog"
    "net/http"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))          // 200 and a small body
    })
    mux.HandleFunc("GET /v1/items/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")        // the path wildcard
        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,   // protects against slow clients
    }
    slog.Info("listening", "addr", srv.Addr)
    if err := srv.ListenAndServe(); err != nil {
        slog.Error("server stopped", "err", err)
    }
}
⚠️
Every http.Server in production needs ReadHeaderTimeout and WriteTimeout. Without them a handful of idle connections can hold resources indefinitely, and ListenAndServe returns http.ErrServerClosed on a clean shutdown — do not treat that value as a failure.

FAQ

Why is my JSON output empty?
Almost always the fields are unexported. Only fields starting with a capital letter are encoded; add a struct tag to control the wire name.
How do I format a date in Go?
Use the reference layout time.RFC3339 or a layout built from the reference time Mon Jan 2 15:04:05 MST 2006. There is no yyyy-MM-dd pattern language.

Building HTTP services Setting up Go: modules, toolchain and editors

Last refreshed 2026-09-18.