14 — Modules & Tooling

How Go projects are structured, versioned, and driven from the command line — the ground the go command covers that in C you'd assemble from make, pkg-config, and manual vendoring. Mostly reference, with one runnable example at the end.

1. Modules and versions

A module is a versioned group of packages, defined by a go.mod at its root. The module path (e.g. github.com/acsecengtesting/go-for-c) doubles as the import prefix for every package inside. go.sum records dependency checksums for reproducible builds; commit both.

Versions are semantic — vMAJOR.MINOR.PATCH — and compared numerically, major first. A new major version changes the import path (.../v2), so incompatible majors can coexist. Go uses minimal version selection: it picks the lowest version satisfying all requirements, so builds are deterministic.

2. Everyday commands

CommandWhat it does
go mod init <path>create a module
go mod tidysync dependencies and go.sum
go get pkg@v1.2.3add/upgrade a dependency
go build ./...compile everything
go test ./...run tests (-race, -bench=., -cover)
go vet ./...static checks
gofmt -w .format (one canonical style)
go run .build and run

Other tooling worth knowing: workspaces (go.work) for multi-module local development, build constraints (//go:build linux) as a typed #ifdef, and cross-compilation via GOOS/GOARCH with no cross-toolchain to install.

3. Semantic version ordering (runnable)

To make the ordering rule concrete, here's a comparator that parses vMAJOR.MINOR.PATCH and compares numerically — note v10.0.0 > v9.0.0, which a string compare would get wrong:

package main

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

func parse(v string) ([3]int, error) {
    var out [3]int
    if !strings.HasPrefix(v, "v") {
        return out, fmt.Errorf("missing v prefix: %q", v)
    }
    parts := strings.Split(v[1:], ".")
    if len(parts) != 3 {
        return out, fmt.Errorf("want 3 fields: %q", v)
    }
    for i, p := range parts {
        n, err := strconv.Atoi(p)
        if err != nil || n < 0 {
            return out, fmt.Errorf("bad field %q", p)
        }
        out[i] = n
    }
    return out, nil
}

func CompareSemver(a, b string) (int, error) {
    pa, err := parse(a)
    if err != nil {
        return 0, err
    }
    pb, err := parse(b)
    if err != nil {
        return 0, err
    }
    for i := 0; i < 3; i++ {
        if pa[i] < pb[i] {
            return -1, nil
        }
        if pa[i] > pb[i] {
            return 1, nil
        }
    }
    return 0, nil
}

func main() {
    for _, pair := range [][2]string{
        {"v1.2.3", "v1.2.4"},
        {"v2.0.0", "v1.9.9"},
        {"v1.0.0", "v1.0.0"},
        {"v10.0.0", "v9.0.0"},
    } {
        r, _ := CompareSemver(pair[0], pair[1])
        fmt.Printf("%s vs %s = %d\n", pair[0], pair[1], r)
    }
    _, err := CompareSemver("1.2.3", "v1.0.0")
    fmt.Println("error:", err)
}

Output:

v1.2.3 vs v1.2.4 = -1
v2.0.0 vs v1.9.9 = 1
v1.0.0 vs v1.0.0 = 0
v10.0.0 vs v9.0.0 = 1
error: missing v prefix: "1.2.3"

That's the course. Back to all modules, or the repository to work the exercises.