The Standard Library You Will Actually Use

Article · 14:00

Go standard library is unusually complete, and leaning on it is idiomatic rather than lazy. The packages that turn up in almost every program: encoding/json and its struct tags, time layouts written by example, io composition, and the slices and maps helpers.

The Standard Library You Will Actually Use

Go's standard library is unusually complete, and leaning on it is idiomatic rather than lazy. This lesson covers the packages that turn up in almost every program.

encoding/json

package main

import (
	"encoding/json"
	"fmt"
	"strings"
)

type Course struct {
	Slug      string   `json:"slug"`
	Title     string   `json:"title"`
	Lessons   int      `json:"lessons"`
	Tags      []string `json:"tags,omitempty"`
	Published bool     `json:"-"`             // never serialised
}

func main() {
	course := Course{Slug: "go", Title: "Go", Lessons: 12, Published: true}

	encoded, err := json.Marshal(course)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(encoded))

	var decoded Course
	if err := json.Unmarshal(encoded, &decoded); err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", decoded)

	// Unknown fields are ignored by default; DisallowUnknownFields is how you
	// catch a typo in a config file instead of silently using the zero value.
	strict := json.NewDecoder(strings.NewReader(`{"slug":"go","lessonz":12}`))
	strict.DisallowUnknownFields()
	var target Course
	fmt.Println("strict decode:", strict.Decode(&target))

	// Decoding into a map when the shape is not known ahead of time.
	var loose map[string]any
	_ = json.Unmarshal([]byte(`{"a":1,"b":["x","y"]}`), &loose)
	fmt.Printf("%T for a number, %T for an array\n", loose["a"], loose["b"])
}

Note the types in that last line: every JSON number decodes into float64 and every array into []any. That is the price of map[string]any, and the reason a struct is almost always better.

time

package main

import (
	"fmt"
	"time"
)

func main() {
	// A fixed instant, so this lesson prints the same thing every run.
	launch := time.Date(2026, time.March, 14, 9, 30, 0, 0, time.UTC)

	fmt.Println(launch.Format(time.RFC3339))
	fmt.Println(launch.Format("Mon 2 Jan 2006 at 15:04"))   // the reference layout

	later := launch.Add(90 * time.Minute)
	fmt.Println(later.Sub(launch), later.After(launch))

	deadline := launch.AddDate(0, 1, 0)
	fmt.Println("a month later:", deadline.Format(time.DateOnly))

	parsed, err := time.Parse(time.RFC3339, "2026-03-14T09:30:00Z")
	fmt.Println(parsed.Equal(launch), err)

	// Durations are typed, which stops the "was that seconds or milliseconds?"
	// class of bug entirely.
	timeout := 250 * time.Millisecond
	fmt.Println(timeout, timeout.Seconds())

	start := time.Now()
	time.Sleep(10 * time.Millisecond)
	fmt.Println("elapsed at least 10ms:", time.Since(start) >= 10*time.Millisecond)
}

That layout string is Go's one real oddity: the reference time is Mon Jan 2 15:04:05 MST 2006 - 01/02 03:04:05PM '06 -0700 - and you write the format by example rather than with %Y.

strings, strconv and bytes

package main

import (
	"fmt"
	"strconv"
	"strings"
)

func main() {
	s := "  Go: From Basics to Expert  "

	fmt.Printf("%q\n", strings.TrimSpace(s))
	fmt.Println(strings.Contains(s, "Basics"), strings.HasPrefix(strings.TrimSpace(s), "Go"))
	fmt.Println(strings.Split("a,b,c", ","))
	fmt.Println(strings.Join([]string{"a", "b", "c"}, " -> "))
	fmt.Println(strings.ReplaceAll("one one one", "one", "1"))
	fmt.Println(strings.Fields("  several   spaced   words  "))
	fmt.Println(strings.EqualFold("GoLang", "golang"))       // ASCII-insensitive

	// strconv converts; it never guesses.
	n, err := strconv.Atoi("42")
	fmt.Println(n, err)

	_, err = strconv.Atoi("42x")
	fmt.Println("error:", err)

	fmt.Println(strconv.Itoa(42) + "!")
	fmt.Println(strconv.FormatFloat(3.14159, 'f', 2, 64))
	fmt.Println(strconv.Quote("needs \"escaping\""))

	b, _ := strconv.ParseBool("true")
	fmt.Println(b)
}

io, and why it composes

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	// Anything that reads is an io.Reader; anything that writes is an io.Writer.
	// A file, a socket, a buffer and a string all are, so code written against
	// them works with all four.
	reader := strings.NewReader("first line\nsecond line\nthird line\n")

	scanner := bufio.NewScanner(reader)
	count := 0
	for scanner.Scan() {
		count++
		fmt.Printf("  %d: %s\n", count, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}

	// io.Copy moves bytes from any reader to any writer without a buffer of
	// your own - here from a string straight to stdout.
	if _, err := io.Copy(os.Stdout, strings.NewReader("copied directly\n")); err != nil {
		panic(err)
	}

	// A writer that counts, in six lines, usable anywhere an io.Writer is.
	counting := &countingWriter{}
	fmt.Fprintf(io.MultiWriter(os.Stdout, counting), "written to two places\n")
	fmt.Println("bytes counted:", counting.n)
}

type countingWriter struct{ n int }

func (w *countingWriter) Write(p []byte) (int, error) {
	w.n += len(p)
	return len(p), nil
}

sort, slices and maps

package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

type Learner struct {
	Name  string
	Score int
}

func main() {
	learners := []Learner{{"Ada", 95}, {"Grace", 88}, {"Alan", 95}}

	// slices.SortFunc (Go 1.21+) replaces most uses of the old sort package.
	slices.SortFunc(learners, func(a, b Learner) int {
		if c := b.Score - a.Score; c != 0 {
			return c                        // descending by score
		}
		return strings.Compare(a.Name, b.Name)
	})
	fmt.Println(learners)

	numbers := []int{5, 2, 9, 1}
	slices.Sort(numbers)
	fmt.Println(numbers, slices.Contains(numbers, 9), slices.Index(numbers, 5))
	fmt.Println(slices.Max(numbers), slices.Min(numbers))
	reversed := slices.Clone(numbers)      // Clone, so the original is untouched
	slices.Reverse(reversed)
	fmt.Println(reversed, numbers)

	stock := map[string]int{"pears": 2, "apples": 5, "cherries": 9}
	keys := slices.Sorted(maps.Keys(stock))     // map iteration is random; sort it
	fmt.Println(keys)

	// maps.Keys and maps.Values return iterators (Go 1.23+), so they are ranged
	// over directly rather than returning a slice.
	total := 0
	for v := range maps.Values(stock) {
		total += v
	}
	fmt.Println("total", total)
}

What to take away

  • encoding/json reads struct tags, and only exported fields; unknown fields are ignored unless you forbid them.
  • A JSON number in a map[string]any is a float64 - prefer a struct.
  • Go's time layouts are written by example, using the reference 2006-01-02 15:04:05.
  • time.Duration is a type, which removes a whole class of unit bug.
  • io.Reader/io.Writer compose: bufio, io.Copy and io.MultiWriter work with anything.
  • slices and maps (Go 1.21+) replace most hand-written sorting and key extraction.