Representative interview topic

Go coding interview: How would you build a low-allocation metadata scanner with Go 1.26 reflect iterators?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

You maintain a reflection-driven Go serializer that scans structs with NumField, Field, and temporary slices. After upgrading to Go 1.26, design a scanner using reflect iterators and explain version compatibility, panic boundaries, unexported fields, and benchmark proof.

Prompt and context

A serializer scans struct fields, methods, and function signatures. The old code calls NumField, indexes Field, and collects results in several temporary slices. Go 1.26 adds field and method iterators returning iter.Seq2 on reflect.Type and reflect.Value. Design the upgrade while preserving older Go versions, addressability rules, and unexported-field behavior.

What the interviewer is testing

  • Whether you distinguish Type metadata from Value data in the iterator pairs.
  • Whether you can consume iter.Seq2 with range without claiming that laziness means free, concurrent, or zero-cost execution.
  • Whether you handle non-struct and invalid Values, unexported fields, CanInterface, and CanSet.
  • Whether you can define version builds, cache keys, benchmarks, and a fallback path.

Clarifying questions to ask first

  1. Is the scanner metadata-only, or must it read and write field values?
  2. What is the module's minimum Go version, and may build tags be used?
  3. Should unexported fields be skipped, rejected, or recorded as metadata only?
  4. Are reflection results cached across requests or loaded from plugins?
  5. Is the primary goal allocations, latency, simplicity, or a combination?

A 30-second answer framework

Go 1.26 adds Type.Fields, Type.Methods, Type.Ins, Type.Outs, Value.Fields, and Value.Methods, all consumable as iter.Seq2. I would use Type iterators to build immutable metadata caches and Value iterators only when an instance value is needed. Check IsValid and Kind at the boundary, CanInterface before exposing values, and CanSet before writing. For older Go versions, keep an index-based implementation selected with build tags and compare both paths with the same correctness and allocation benchmarks.

Step-by-step deep answer

Step 1: Choose Type or Value iterators

Type.Fields and Type.Methods enumerate type descriptions; Type.Ins and Type.Outs enumerate function parameters and results. Value.Fields and Value.Methods yield metadata together with the corresponding Value. Cache Type results when compiling a schema; consume Value iterators for an instance so request state never enters a global cache.

Step 2: Consume Seq2 with range

An iterator is an iter.Seq2[A, B], so callers need no index protocol. Keep scan state local:

go
func fieldsOf(v reflect.Value) ([]string, error) {
    if !v.IsValid() || v.Kind() != reflect.Struct {
        return nil, errors.New("expected struct")
    }
    names := make([]string, 0, v.NumField())
    for sf, fv := range v.Fields() {
        if sf.PkgPath != "" || !fv.CanInterface() {
            continue
        }
        names = append(names, sf.Name)
    }
    return names, nil
}

The iterator removes index boilerplate, but it does not make slices, string conversions, or interface boxing allocation-free. Measure the whole scanner.

Step 3: Define panic and addressability boundaries

Value.Fields requires a Struct Kind and panics for invalid input; check IsValid first. Type information for unexported fields is usually readable, but Interface can panic when a value is not accessible, and writes also require CanSet. A library should return explicit errors or recover at a controlled boundary instead of leaking a reflection panic into a request handler.

Step 4: Design metadata caches

Use reflect.Type as a cache key and store exported fields, tags, indexes, and conversion functions. Store immutable descriptions, never a request's Value. Track types currently being built to stop recursive types from causing infinite recursion. Use a read-only snapshot or sync.Map for concurrent reads and publish a new description atomically.

Step 5: Handle function signatures

Check Kind() == reflect.Func before consuming Ins and Outs in declaration order. A variadic function's final input remains a slice type; it is not an arbitrary number of arguments. When method iterators yield method metadata and a method Value, decide whether the receiver and bound method value belong in the cache.

Step 6: Plan older-version compatibility

If the module supports a version older than Go 1.26, put the iterator and index implementations in separate files selected by //go:build go1.26 and the inverse tag. Both paths must emit the same field order, tags, and errors. Do not discover API availability at runtime; an old compiler rejects a source file that references the new methods.

Step 7: Prove the benefit with benchmarks

Benchmark small and nested structs, unexported fields, function signatures, and cache hits and misses. Record allocs/op, B/op, ns/op, and output equality. If iterators only remove boilerplate without reducing allocations, keep the simpler path or optimize surrounding slices and interface conversions instead of claiming “naturally zero allocation.”

High-quality sample answer

I would use Type iterators to build cacheable field, method, and signature metadata, and restrict Value iterators to instance reads. The boundary checks IsValid and Kind; CanInterface and CanSet guard exposure and writes, while unexported fields are skipped or reported explicitly. Go 1.26 consumes iter.Seq2 with range, and build tags preserve an index fallback for older versions. Identical ordering and error contracts let benchmarks compare allocations, latency, and cache behavior honestly.

Common mistakes

  • Treating Type.Fields as an API that returns field values.
  • Calling Value.Fields without checking Kind or IsValid.
  • Calling Interface on an unexported field without CanInterface.
  • Treating an iterator as zero-allocation, thread-safe, or a reusable result slice.
  • Trying to probe new methods at runtime instead of isolating source with build tags.

Follow-up questions and responses

Follow-up 1: Why not use VisibleFields?

VisibleFields returns a flattened slice and is useful when the complete embedded-field set is needed at once. Value iterators also provide instance values without first building a result slice. Choose based on the workload and measured results.

Follow-up 2: Can an iterator be shared across goroutines?

Do not assume that. Consume it within a clear Type or Value lifetime, cache immutable metadata, and avoid reusing a Value carrying instance state or a partially consumed iterator.

Follow-up 3: How do you handle embedded fields and shadowing?

Retain StructField.Index and the anonymous marker, then apply Go visibility rules to produce an unambiguous access path. If flattening is required, define conflict policy during metadata compilation and test shadowing order.

Follow-up 4: How do you prove older versions still work?

In CI, build both tagged implementations with the minimum supported Go version and Go 1.26, then run the same golden outputs and benchmarks. Any new API in an old-version compilation unit fails immediately.

Follow-up 5: When should reflection become code generation?

When the type set is stable, latency is critical, and generation is operationally controlled, generated code is usually more predictable. Iterators reduce boilerplate first; benchmarks and deployment constraints should decide whether runtime reflection remains appropriate.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool