← Interview Prep

Go Mock — Live-Coding Tasks

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.

Task 1 — Config parser

Implement ParseInterfaces: return a slice of Interface in the order the blocks appear in the config. For each interface block extract:

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.

Approach

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.

Solution (Go)

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
}

Pitfalls

Task 2 — IP tools (aggregation, overlaps, LPM)

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.

Approach

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).

Solution (Go)

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
}

Pitfalls

Task 3 — Worker pool

Implement Collect(ctx, devices, workers, fn) map[string]Result, a worker pool that polls devices. Requirements:

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)?

Approach

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.

Solution (Go)

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
}

Pitfalls

Task 4 — Flatten nested structure + path diff

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:

Careful: JSON numbers arrive as float64 — in a path key, 9214 must print as "9214", not "9214.000000".

Approach

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.

Solution (Go)

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
}

Pitfalls

Related: TCP — Theory & Mechanisms · Life of a Packet.