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
| Command | What it does |
|---|---|
go mod init <path> | create a module |
go mod tidy | sync dependencies and go.sum |
go get pkg@v1.2.3 | add/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.