← Interview Prep

Go Drills — Fundamentals

Four short, codepad-sized Go problems from the network-automation domain — pure functions, stdlib only. Written to be solved in a plain editor under a timer, then pasted into src/ and tested.

Each drill is a self-contained package: a doc comment states the task, and the reference solution in solutions/ is what you should be able to reproduce cold. The whole point is to catch your habitual mistakes dry — no interpreter, no linter, no autocomplete — because those are exactly the ones that surface in a live-coding pad.

Range expand / compress

Problem. VLAN ranges. Target: 10 minutes, written in a notepad.

Approach

Expand: split on ",", trim each chunk, strings.Cut on "-" to detect a range. Collect into a map[int]bool to dedupe, then dump keys into a slice and sort.Ints — because map iteration order is random. Validate every strconv.Atoi and reject reversed ranges.

Compress: dedupe + sort first, then walk the sorted slice tracking a [start, prev] run, flushing a "a-b" (or single "a") part whenever the sequence breaks. Do not forget the final flush after the loop.

Complexity: O(n log n) dominated by the sort; O(n) space.

Solution (Go)

package d1ranges

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

// ExpandRanges turns "10,20,30-32" into [10 20 30 31 32].
func ExpandRanges(spec string) ([]int, error) {
	seen := map[int]bool{}
	for _, chunk := range strings.Split(spec, ",") {
		chunk = strings.TrimSpace(chunk)
		if chunk == "" {
			continue
		}
		lo, hi, isRange := strings.Cut(chunk, "-")
		if !isRange {
			v, err := strconv.Atoi(chunk)
			if err != nil {
				return nil, fmt.Errorf("bad number %q: %w", chunk, err)
			}
			seen[v] = true
			continue
		}
		start, err := strconv.Atoi(strings.TrimSpace(lo))
		if err != nil {
			return nil, fmt.Errorf("bad range %q: %w", chunk, err)
		}
		end, err := strconv.Atoi(strings.TrimSpace(hi))
		if err != nil {
			return nil, fmt.Errorf("bad range %q: %w", chunk, err)
		}
		if start > end {
			return nil, fmt.Errorf("reversed range %q", chunk)
		}
		for v := start; v <= end; v++ {
			seen[v] = true
		}
	}
	out := make([]int, 0, len(seen))
	for v := range seen {
		out = append(out, v)
	}
	sort.Ints(out)
	return out, nil
}

// CompressRanges turns [10 11 12 15] into "10-12,15".
func CompressRanges(nums []int) string {
	if len(nums) == 0 {
		return ""
	}
	uniq := make([]int, 0, len(nums))
	seen := map[int]bool{}
	for _, n := range nums {
		if !seen[n] {
			seen[n] = true
			uniq = append(uniq, n)
		}
	}
	sort.Ints(uniq)

	var parts []string
	start, prev := uniq[0], uniq[0]
	flush := func() {
		if start == prev {
			parts = append(parts, strconv.Itoa(start))
		} else {
			parts = append(parts, fmt.Sprintf("%d-%d", start, prev))
		}
	}
	for _, n := range uniq[1:] {
		if n == prev+1 {
			prev = n
			continue
		}
		flush()
		start, prev = n, n
	}
	flush()
	return strings.Join(parts, ",")
}

Pitfalls

MAC normalization

Problem. MAC addresses. Target: 10 minutes.

Hint: a strings.Builder plus a per-character check reads better than a regexp and pulls in no extra package. Signatures return (T, error) — idiomatic, no panics.

Approach

Centralize parsing in one digits helper: lowercase the input, skip separators (:, ., -, space, tab), keep 0-9a-f, reject anything else, and require exactly 12 hex digits. Every public function funnels through it, so validation lives in one place. The I/G and U/L bits come from parsing the first octet with strconv.ParseUint(d[:2], 16, 8) and masking &0b1 / &0b10.

Complexity: O(n) over the input length, constant extra space.

Solution (Go)

package d2mac

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

func digits(mac string) (string, error) {
	var b strings.Builder
	for _, r := range strings.ToLower(mac) {
		switch {
		case r == ':' || r == '.' || r == '-' || r == ' ' || r == '\t':
			continue
		case (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f'):
			b.WriteRune(r)
		default:
			return "", fmt.Errorf("invalid MAC %q", mac)
		}
	}
	if b.Len() != 12 {
		return "", fmt.Errorf("invalid MAC %q", mac)
	}
	return b.String(), nil
}

// NormalizeMAC returns the canonical aa:bb:cc:dd:ee:ff form.
func NormalizeMAC(mac string) (string, error) {
	d, err := digits(mac)
	if err != nil {
		return "", err
	}
	parts := make([]string, 0, 6)
	for i := 0; i < 12; i += 2 {
		parts = append(parts, d[i:i+2])
	}
	return strings.Join(parts, ":"), nil
}

// ToCisco returns the aabb.ccdd.eeff form.
func ToCisco(mac string) (string, error) {
	d, err := digits(mac)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%s.%s.%s", d[0:4], d[4:8], d[8:12]), nil
}

func firstOctet(mac string) (uint64, error) {
	d, err := digits(mac)
	if err != nil {
		return 0, err
	}
	return strconv.ParseUint(d[:2], 16, 8)
}

// IsMulticast reports the I/G bit.
func IsMulticast(mac string) (bool, error) {
	o, err := firstOctet(mac)
	if err != nil {
		return false, err
	}
	return o&0b1 != 0, nil
}

// IsLocallyAdministered reports the U/L bit.
func IsLocallyAdministered(mac string) (bool, error) {
	o, err := firstOctet(mac)
	if err != nil {
		return false, err
	}
	return o&0b10 != 0, nil
}

Pitfalls

Counter deltas + errors.Is

Problem. Interface counters. Target: 12 minutes.

Talk through it separately: why uint64 and not int, and what would happen to an int on wraparound.

Approach

Delta validates the width first and wraps the sentinel with fmt.Errorf("%w, ...", ErrBadWidth) so callers can match it via errors.Is. The 32-bit wrap is exact modular arithmetic in uint64; the 64-bit "reset" case returns 0 because there is no larger width to wrap into. RateBps guards dt, reuses Delta, and multiplies by 8 in float64 to get bits/sec. TopTalkers maps samples to rates then uses sort.Slice with an explicit tie-breaker, and clamps n to the slice length.

Complexity: Delta/RateBps are O(1); TopTalkers is O(n log n).

Solution (Go)

package d3counters

import (
	"errors"
	"fmt"
	"sort"
)

// ErrBadWidth is returned for unsupported counter widths.
var ErrBadWidth = errors.New("width must be 32 or 64")

// Delta computes the counter increment, handling 32-bit wrap and 64-bit resets.
func Delta(prev, curr uint64, width int) (uint64, error) {
	if width != 32 && width != 64 {
		return 0, fmt.Errorf("%w, got %d", ErrBadWidth, width)
	}
	if curr >= prev {
		return curr - prev, nil
	}
	if width == 32 {
		return (1 << 32) - prev + curr, nil
	}
	return 0, nil
}

// RateBps converts a byte counter pair into bits per second.
func RateBps(prev, curr uint64, dt float64, width int) (float64, error) {
	if dt <= 0 {
		return 0, errors.New("dt must be positive")
	}
	d, err := Delta(prev, curr, width)
	if err != nil {
		return 0, err
	}
	return float64(d) * 8 / dt, nil
}

// Sample is a prev/curr counter pair for one interface.
type Sample struct {
	Iface      string
	Prev, Curr uint64
}

// Talker is an interface with its computed rate.
type Talker struct {
	Iface string
	Bps   float64
}

// TopTalkers returns the n fastest interfaces, ties broken by name.
func TopTalkers(samples []Sample, dt float64, n int) ([]Talker, error) {
	out := make([]Talker, 0, len(samples))
	for _, s := range samples {
		bps, err := RateBps(s.Prev, s.Curr, dt, 64)
		if err != nil {
			return nil, err
		}
		out = append(out, Talker{Iface: s.Iface, Bps: bps})
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Bps != out[j].Bps {
			return out[i].Bps > out[j].Bps
		}
		return out[i].Iface < out[j].Iface
	})
	if n > len(out) {
		n = len(out)
	}
	return out[:n], nil
}

Pitfalls

LLDP topology + BFS

Problem. Topology from LLDP + BFS. Target: 15 minutes.

Path reconstruction: keep prev[node] = parent, then reverse the slice.

Approach

BuildTopology parses each line with strings.Fields, rejects malformed and self-link lines, and records both directions in a nested map[string]map[string]bool (lazy-initializing the inner map). ShortestPath runs breadth-first from src, keeping a prev map both as the visited set and as parent pointers. The critical detail: sort each node's neighbors with sort.Strings before enqueueing, so traversal — and therefore the returned path — is deterministic. On reaching dst, walk prev back to src and reverse.

Complexity: O(V + E) BFS, plus O(deg log deg) per node for the neighbor sort.

Solution (Go)

package d4topology

import (
	"sort"
	"strings"
)

// Topology is an undirected adjacency map.
type Topology map[string]map[string]bool

// BuildTopology parses "local lport neighbor rport" lines.
func BuildTopology(lines []string) Topology {
	topo := Topology{}
	add := func(a, b string) {
		if topo[a] == nil {
			topo[a] = map[string]bool{}
		}
		topo[a][b] = true
	}
	for _, line := range lines {
		f := strings.Fields(line)
		if len(f) != 4 || f[0] == f[2] {
			continue
		}
		add(f[0], f[2])
		add(f[2], f[0])
	}
	return topo
}

// ShortestPath returns a BFS path from src to dst, or nil if none exists.
func ShortestPath(topo Topology, src, dst string) []string {
	if _, ok := topo[src]; !ok {
		return nil
	}
	if _, ok := topo[dst]; !ok {
		return nil
	}
	if src == dst {
		return []string{src}
	}

	prev := map[string]string{src: src}
	queue := []string{src}

	for len(queue) > 0 {
		node := queue[0]
		queue = queue[1:]

		neighbors := make([]string, 0, len(topo[node]))
		for nb := range topo[node] {
			neighbors = append(neighbors, nb)
		}
		sort.Strings(neighbors)

		for _, nb := range neighbors {
			if _, seen := prev[nb]; seen {
				continue
			}
			prev[nb] = node
			if nb == dst {
				path := []string{dst}
				for path[len(path)-1] != src {
					path = append(path, prev[path[len(path)-1]])
				}
				for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
					path[i], path[j] = path[j], path[i]
				}
				return path
			}
			queue = append(queue, nb)
		}
	}
	return nil
}

Pitfalls

Drills from drills/go — statements in src/, reference solutions in solutions/. Solve dry under the timer, then go test ./src/... and go vet ./src/... && gofmt -l ./src.