Four network-engineering live-coding problems in Go, each with a clean idiomatic reference solution: a config parser, IP/prefix tooling, a worker pool, and a JSON-to-gNMI flattener.
These are the kind of short, self-contained tasks a network-automation or SRE interview throws at you: parse
device config, do CIDR math, fan out work concurrently, and reshape a nested document. Each one is scoped to
15–25 minutes. What the interviewer actually watches for is idiomatic Go — reaching for the
standard library (strings, net/netip, context) before regex or hand-rolled
math, avoiding needless allocations, and getting concurrency correct under -race.
Implement ParseInterfaces: return a slice of Interface in the order the
blocks appear in the config. For each interface block extract:
description .mtu N, otherwise 0.ip address X, otherwise "".false on shutdown, defaults to true.switchport trunk allowed vlan 10,20,30-32 expands to [10 20 30 31 32].Ignore blank lines, !, lines containing #, and any line outside an interface block.
The interviewer is looking at idiomatic parsing (strings vs regexp), lack of extra
allocations, and working through the index of the current element instead of copying the struct around.
Split the config into lines, trim each, and skip noise. A line matching interface <name>
appends a fresh element (with Enabled: true) and advances a cur index; every subsequent
attribute line mutates out[cur] in place. VLAN ranges expand through a set to dedupe,
then sort. Regexps are compiled once at package scope. Complexity: O(n) in the
number of lines (VLAN expansion is O(v log v) for the sort per trunk line); one pass, no per-line
struct copies.
package task1parser
import (
"regexp"
"sort"
"strconv"
"strings"
)
// Interface is a parsed interface block.
type Interface struct {
Name string
Description string
MTU int
IPv4 string
Enabled bool
VLANs []int
}
var (
reIface = regexp.MustCompile(`^interface\s+(\S+)$`)
reDesc = regexp.MustCompile(`^description\s+(.+)$`)
reMTU = regexp.MustCompile(`^mtu\s+(\d+)$`)
reIPv4 = regexp.MustCompile(`^ip address\s+(\S+)$`)
reVLAN = regexp.MustCompile(`^switchport (?:trunk allowed vlan|access vlan)\s+(\S+)$`)
)
func expandVLANs(spec string) []int {
seen := map[int]bool{}
for _, chunk := range strings.Split(spec, ",") {
chunk = strings.TrimSpace(chunk)
if chunk == "" {
continue
}
if lo, hi, ok := strings.Cut(chunk, "-"); ok {
start, err1 := strconv.Atoi(lo)
end, err2 := strconv.Atoi(hi)
if err1 != nil || err2 != nil {
continue
}
for v := start; v <= end; v++ {
seen[v] = true
}
continue
}
if v, err := strconv.Atoi(chunk); err == nil {
seen[v] = true
}
}
out := make([]int, 0, len(seen))
for v := range seen {
out = append(out, v)
}
sort.Ints(out)
return out
}
// ParseInterfaces returns interfaces in the order they appear in the config.
func ParseInterfaces(config string) []Interface {
var out []Interface
cur := -1
for _, raw := range strings.Split(config, "\n") {
line := strings.TrimSpace(raw)
if line == "" || line == "!" || strings.HasPrefix(line, "#") {
continue
}
if m := reIface.FindStringSubmatch(line); m != nil {
out = append(out, Interface{Name: m[1], Enabled: true, VLANs: []int{}})
cur = len(out) - 1
continue
}
if cur < 0 {
continue
}
switch {
case reDesc.MatchString(line):
out[cur].Description = reDesc.FindStringSubmatch(line)[1]
case reMTU.MatchString(line):
out[cur].MTU, _ = strconv.Atoi(reMTU.FindStringSubmatch(line)[1])
case reIPv4.MatchString(line):
out[cur].IPv4 = reIPv4.FindStringSubmatch(line)[1]
case line == "shutdown":
out[cur].Enabled = false
case line == "no shutdown":
out[cur].Enabled = true
case reVLAN.MatchString(line):
out[cur].VLANs = expandVLANs(reVLAN.FindStringSubmatch(line)[1])
}
}
return out
}
cur, not a map[name]Interface —
a map loses insertion order and the tests check order.out[cur].Field = .... Taking a copy
(iface := out[cur]) and forgetting to write it back silently drops updates.Enabled must default to true — set it when the block is
created, not only on no shutdown.30-32 inclusively and dedupe overlapping ranges; a bare
strings.Split(",") without range expansion fails the trunk case.^...$) so ip address doesn't
accidentally match a description that contains the words.interface header (cur < 0) must be a
no-op, not a panic.Work with prefixes through net/netip.
Summarize(prefixes) ([]string, error) aggregates prefixes into the minimal
covering set: drop nested prefixes, and merge adjacent halves into their supernet, repeating until stable. For
example ["10.0.0.0/26","10.0.0.64/26","10.0.0.128/25"] becomes ["10.0.0.0/24"]. Output
all IPv4 in ascending order first, then IPv6. Invalid input returns an error (wrapped with
fmt.Errorf("...: %w", err)).
LongestPrefixMatch(routes, addr) (string, error) returns the most specific route
covering addr; no match returns ("", nil); an invalid address returns an
error.
Hints from the brief: netip.ParsePrefix, Prefix.Masked(),
Prefix.Contains(), Addr.Compare(), and
netip.PrefixFrom(addr, bits-1).Masked() to get the supernet.
Parse everything to canonical (masked) prefixes up front, wrapping parse errors. Split into IPv4 and IPv6
families and summarize each independently. Within a family: sort by address then prefix length, drop any prefix
contained in its predecessor, then repeatedly scan adjacent pairs — two same-length prefixes that share a supernet
and differ in address are sibling halves, so collapse them and re-run the contained-drop, looping until a full pass
merges nothing. LPM is a linear scan keeping the longest matching prefix of the same family.
Complexity: summarize is O(n log n) per merge round and converges in
O(log(maxbits)) rounds; LPM is O(n).
package task2ip
import (
"fmt"
"net/netip"
"sort"
)
func parseAll(in []string) ([]netip.Prefix, error) {
out := make([]netip.Prefix, 0, len(in))
for _, s := range in {
p, err := netip.ParsePrefix(s)
if err != nil {
return nil, fmt.Errorf("invalid prefix %q: %w", s, err)
}
out = append(out, p.Masked())
}
return out, nil
}
func sortPrefixes(ps []netip.Prefix) {
sort.Slice(ps, func(i, j int) bool {
if c := ps[i].Addr().Compare(ps[j].Addr()); c != 0 {
return c < 0
}
return ps[i].Bits() < ps[j].Bits()
})
}
func dropContained(ps []netip.Prefix) []netip.Prefix {
sortPrefixes(ps)
var out []netip.Prefix
for _, p := range ps {
if n := len(out); n > 0 {
last := out[n-1]
if last.Bits() <= p.Bits() && last.Contains(p.Addr()) {
continue
}
}
out = append(out, p)
}
return out
}
func supernet(p netip.Prefix) netip.Prefix {
return netip.PrefixFrom(p.Addr(), p.Bits()-1).Masked()
}
func canMerge(a, b netip.Prefix) bool {
if a.Bits() != b.Bits() || a.Bits() == 0 {
return false
}
if a.Addr().Is4() != b.Addr().Is4() {
return false
}
sa, sb := supernet(a), supernet(b)
return sa == sb && a.Addr() != b.Addr()
}
func summarizeFamily(ps []netip.Prefix) []netip.Prefix {
ps = dropContained(ps)
for {
merged := false
var out []netip.Prefix
for i := 0; i < len(ps); i++ {
if i+1 < len(ps) && canMerge(ps[i], ps[i+1]) {
out = append(out, supernet(ps[i]))
i++
merged = true
continue
}
out = append(out, ps[i])
}
ps = dropContained(out)
if !merged {
return ps
}
}
}
// Summarize aggregates prefixes into the minimal covering set: IPv4 first, then IPv6.
func Summarize(prefixes []string) ([]string, error) {
ps, err := parseAll(prefixes)
if err != nil {
return nil, err
}
var v4, v6 []netip.Prefix
for _, p := range ps {
if p.Addr().Is4() {
v4 = append(v4, p)
} else {
v6 = append(v6, p)
}
}
res := append(summarizeFamily(v4), summarizeFamily(v6)...)
out := make([]string, 0, len(res))
for _, p := range res {
out = append(out, p.String())
}
return out, nil
}
// LongestPrefixMatch returns the most specific route covering addr, or "" if none.
func LongestPrefixMatch(routes []string, addr string) (string, error) {
ip, err := netip.ParseAddr(addr)
if err != nil {
return "", fmt.Errorf("invalid address %q: %w", addr, err)
}
ps, err := parseAll(routes)
if err != nil {
return "", err
}
best := netip.Prefix{}
found := false
for _, p := range ps {
if p.Addr().Is4() != ip.Is4() {
continue
}
if p.Contains(ip) && (!found || p.Bits() > best.Bits()) {
best, found = p, true
}
}
if !found {
return "", nil
}
return best.String(), nil
}
Masked() on parse — 10.0.0.5/24
and 10.0.0.0/24 must compare equal, and supernet must re-mask the shifted prefix./26s collapse to a /25 only if they
share the /25 supernet and differ (a.Addr() != b.Addr()) — otherwise you
merge a prefix with itself or with an unrelated neighbor./26 + /26 -> /25
may then pair with an existing /25. Re-drop-contained and loop until nothing merges.Is4() equality and
Bits() == 0 so you don't build a nonsensical supernet of the default route.("", nil), not an error.%w so callers can errors.Is/As them.Implement Collect(ctx, devices, workers, fn) map[string]Result, a worker pool that polls devices.
Requirements:
workers goroutines, no more (a test verifies this).ctx is already cancelled, unhandled devices get Result{Err: ctx.Err()}.workers < 1 is treated as 1.go test -race must be clean.Hints: a jobs channel plus sync.WaitGroup and a sync.Mutex around the map, with a
select on <-ctx.Done() before calling fn. Interview follow-ups: how does
this differ from errgroup? What happens if fn blocks forever (do you need a timeout
inside fn)?
Spawn exactly workers goroutines, each draining a shared unbuffered jobs channel until
it closes. The main goroutine feeds every device onto the channel, then closes it and wg.Wait()s.
Each worker checks ctx.Done() with a non-blocking select before invoking fn;
if cancelled it records ctx.Err() instead. Writes into the shared map are guarded by a mutex.
Complexity: O(d) work over d devices with a fixed workers
concurrency ceiling and O(d) memory for the results map.
package task3pool
import (
"context"
"sync"
)
// Result holds either a value or an error for a single device.
type Result struct {
Value string
Err error
}
// FetchFunc talks to one device.
type FetchFunc func(ctx context.Context, device string) (string, error)
// Collect runs fn against every device with at most `workers` in flight.
func Collect(ctx context.Context, devices []string, workers int, fn FetchFunc) map[string]Result {
if workers < 1 {
workers = 1
}
jobs := make(chan string)
results := make(map[string]Result, len(devices))
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for dev := range jobs {
var res Result
select {
case <-ctx.Done():
res = Result{Err: ctx.Err()}
default:
v, err := fn(ctx, dev)
res = Result{Value: v, Err: err}
}
mu.Lock()
results[dev] = res
mu.Unlock()
}
}()
}
for _, d := range devices {
jobs <- d
}
close(jobs)
wg.Wait()
return results
}
workers goroutines: spawn the fixed pool once; do not
go per device (that's unbounded concurrency and fails the count test).map written from multiple goroutines is a race — hold
mu on every write, or collect through a results channel instead.range jobs loops.wg.Add before go, defer wg.Done()
inside; wg.Wait() only after close(jobs) or you deadlock.ctx.Done() before each fn so an
already-cancelled context still yields a Result{Err: ctx.Err()} for every device rather than
dropping some.errgroup (first error cancels the group), here
one failure is just stored — record it and keep going.fn blocks its worker indefinitely; the pool itself can't time it out, so any deadline
must live inside fn via the passed ctx.Implement Flatten(data any, prefix string) map[string]any, where data is the result
of json.Unmarshal into any (a map[string]any, []any, or a
scalar). Produce flat gNMI-style paths:
map[string]any → prefix + "/" + key.[]any: an element that is a map with a scalar key name / index
/ id (checked in that order) → prefix + "[name=<value>]"; otherwise
→ prefix + "[<index>]".Careful: JSON numbers arrive as float64 — in a path key, 9214 must print as
"9214", not "9214.000000".
Recurse on the dynamic type. For a map, extend the prefix with /key and merge each child's result.
For a slice, pick a key path per element: if it's a map, look for the first scalar name/index/id
and build a [key=value] keyed path; otherwise fall back to the positional [i] index. For a
scalar, deposit data at the accumulated prefix. A helper stringifies scalars, rendering
integral float64s without a decimal tail. Complexity: O(n) in the total
number of nodes; recursion depth equals the nesting depth.
package task4flatten
import "fmt"
var keyCandidates = []string{"name", "index", "id"}
func listKey(item map[string]any) (string, any, bool) {
for _, k := range keyCandidates {
v, ok := item[k]
if !ok {
continue
}
switch v.(type) {
case map[string]any, []any:
continue
}
return k, v, true
}
return "", nil, false
}
func scalarString(v any) string {
if f, ok := v.(float64); ok && f == float64(int64(f)) {
return fmt.Sprintf("%d", int64(f))
}
return fmt.Sprintf("%v", v)
}
// Flatten converts a decoded JSON structure into flat gNMI-like paths.
func Flatten(data any, prefix string) map[string]any {
out := map[string]any{}
switch v := data.(type) {
case map[string]any:
for key, val := range v {
for k, res := range Flatten(val, prefix+"/"+key) {
out[k] = res
}
}
case []any:
for i, item := range v {
path := fmt.Sprintf("%s[%d]", prefix, i)
if m, ok := item.(map[string]any); ok {
if key, val, ok := listKey(m); ok {
path = fmt.Sprintf("%s[%s=%s]", prefix, key, scalarString(val))
}
}
for k, res := range Flatten(item, path) {
out[k] = res
}
}
default:
out[prefix] = data
}
return out
}
float64; guard integral
values (f == float64(int64(f))) and print with %d so a key reads 9214,
not 9214.000000.name/index/id, skip
candidates whose value is itself a map or slice — otherwise you'd stringify a nested object into the path.name then index then id;
return the first match rather than letting a later key win.[i] — don't drop it
or collapse duplicates.out;
returning a child map directly loses siblings.nil and empty containers are edge cases — an empty map or slice contributes no paths, and a bare
scalar with an empty prefix still needs a sensible key.