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.
Problem. VLAN ranges. Target: 10 minutes, written in a notepad.
ExpandRanges("10,20,30-32") → [10 20 30 31 32], no duplicates, sorted.
Skip empty chunks; "abc", "10-2", "1-x" → error.CompressRanges([10 11 12 15]) → "10-12,15"; [1 2 4] → "1-2,4";
[9] → "9"; nil → "".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.
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, ",")
}
map yields keys in random order, so a test that
expects a stable slice will pass "sometimes" and fail others. Always sort.Ints after collecting
from a map — this is the single most common codepad failure.flush() call after the
loop ends. Forget it and "10-12,15" loses its trailing 15.strconv.Atoi. Every parse returns (int, error); ignoring the
error means garbage numbers slip through instead of surfacing as an error.Problem. MAC addresses. Target: 10 minutes.
NormalizeMAC → "aa:bb:cc:dd:ee:ff" from any of these formats:
AA:BB:CC:DD:EE:FF / aabb.ccdd.eeff / aa-bb-cc-dd-ee-ff /
AABBCCDDEEFF / with spaces. Not exactly 12 hex chars → error.ToCisco → "aabb.ccdd.eeff".IsMulticast → low bit of the first octet (I/G bit).IsLocallyAdministered → second-lowest bit (U/L bit).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.
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.
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
}
(T, error). Reaching for
panic on bad input is a red flag in an interview — thread the error back and let the caller decide.err. Each d, err := digits(...) re-declares in its own scope; that
is fine here, but be deliberate — a stray := inside a nested block silently shadows the outer
err and you end up checking the wrong variable.digits, so a malformed MAC
can never reach the bit-masking logic with fewer than 12 characters.Problem. Interface counters. Target: 12 minutes.
Delta(prev, curr uint64, width int) (uint64, error):
curr >= prev → curr - prev.curr < prev, width 32 → 1<<32 - prev + curr (wraparound).curr < prev, width 64 → 0 (counter reset).ErrBadWidth (the test checks errors.Is!).RateBps(prev, curr, dt, width) → bits/sec; dt <= 0 → error.TopTalkers(samples []Sample, dt float64, n int) ([]Talker, error): sort by descending
Bps, ties broken by name; n larger than the input length returns everything, no panic.Talk through it separately: why uint64 and not int, and what would happen to an int
on wraparound.
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).
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
}
errors.Is, not ==. Wrap the sentinel with %w so
errors.Is(err, ErrBadWidth) unwraps and matches. A plain fmt.Errorf("%v", ...) flattens the
chain and the test's errors.Is fails.float64(d) * 8 / dt before dividing;
computing the rate in integer arithmetic truncates and reports the wrong bps.uint64. Counters are unsigned and can legitimately exceed int's range;
on an int the wrap subtraction could go negative and the modular 1<<32 trick would not hold.Problem. Topology from LLDP + BFS. Target: 15 minutes.
BuildTopology(lines) → Topology: a line is
"leaf1 Ethernet1 spine1 Ethernet1"; the graph is undirected; lines that are not 4 fields and
self-links are skipped (strings.Fields).ShortestPath(topo, src, dst) → []string: BFS; src == dst →
[]string{src}; missing node or no path → nil. Neighbors are visited in
lexicographic order (sort.Strings) — otherwise the result is non-deterministic, because Go's map
iteration order is random. This is a favorite interview question.Path reconstruction: keep prev[node] = parent, then reverse the slice.
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.
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
}
topo[node] yields neighbors in random
order, so among equal-length paths you would return a different one each run. sort.Strings the
neighbor slice to make traversal deterministic — the interviewer's favorite trap.topo[a][b] = true when topo[a] is nil panics;
lazy-initialize the inner map (if topo[a] == nil { topo[a] = map[string]bool{} }) before writing.for _, v := range s { v.X = 1 } mutates a
copy and is lost — index with s[i].X = 1 when you need to modify slice elements in place.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.