Go Interface Interview Questions: Typed nil, Method Sets, and Error Values

Prepare for Go interface interviews with tested typed-nil puzzles, method-set failures, nil receivers, error wrapping, and version-aware matching APIs.

Author: PracHub

Published: 9/21/2026

Go Interface Interview Questions: Typed nil, Method Sets, and Error Values

September 21, 2026

Quick Overview

An interface is nil only when it has no dynamic type and value; method sets and error construction define the safe boundary.

Software EngineerFree

Go interface interview questions test whether you can separate a variable's static type from the concrete value it holds. A typed nil pointer inside an interface is not a nil interface. A method call that works on a local variable does not necessarily prove that the variable's value implements an interface. An error message that looks familiar does not establish error identity.

A reliable answer names the dynamic type, checks the relevant method set, and follows the error's wrapping structure. This guide builds those habits through small, executable counterexamples. For a broader preparation path, see Golang Interview Questions for Backend Engineers.

Evidence boundary: Official facts are attributed to current Go documentation below. Candidate reports: no verified report establishes these as questions from a particular employer. Inference: the examples, debugging sequence, and interview recommendations are original practice material. We checked the fixtures using Go 1.27.1 and identify the newer error API separately.

A classic study desk with paired type and value cards illustrating the two parts of an interface value

Why is a typed nil error not equal to nil?

Official fact: the Go FAQ explains interface nilness through its dynamic type and value. A nil pointer can supply a concrete dynamic type, making the resulting interface non-nil.

Use this original error type and predict the three printed booleans:

type Problem struct { Code string }

func (p *Problem) Error() string {
    if p == nil { return "nil Problem receiver" }
    return p.Code
}

func badResult() error {
    var p *Problem
    return p
}

func goodResult() error {
    return nil
}

// Inside main:
var p *Problem
var err error = p
fmt.Println(p == nil, err == nil, goodResult() == nil)
fmt.Println(badResult() == nil)

The output is true false true, followed by false. Assigning p into err preserves the concrete type *Problem. Returning the same pointer through an error result performs the same conversion, so the caller sees a non-nil error even though the pointer is nil.

The practical bug can appear in a function that declares a concrete error pointer, fills it only on failure, and returns it unconditionally. Our recommendation is to return an explicit nil on the success path and a concrete error only on failure. That makes the intended contract visible at the boundary.

Do not repair every caller with reflection-based nil detection. That spreads knowledge of the provider's mistake throughout the codebase and can blur the difference between an absent object and a legitimate object with a nil internal field. Fix the producer when its documented success condition is a nil error.

Interface stateEqual to nil?
Unset interfaceYes
Typed nil pointerNo
Non-nil Problem pointerNo
Explicit nil returnYes

Can a method be called on a nil receiver?

Our Problem.Error method deliberately accepts a nil receiver and returns a useful string. The call can enter that pointer-receiver method because there is a concrete receiver type to dispatch to. The nil check inside the method prevents dereferencing the receiver.

This is a property of this implementation, not a universal guarantee that methods on nil pointers are safe. A method that immediately reads p.Code would panic when p is nil. A nil interface has no concrete method implementation to dispatch to at all. Separate these cases before making a claim about “nil methods.”

Try adding fmt.Println(err.Error()) to the previous fixture. It prints nil Problem receiver. That output proves the guard ran; it does not turn err into a nil interface or make returning it from a successful operation correct.

For an interview API-design follow-up, explain whether nil is a documented usable receiver or a programming error. A tree node helper might intentionally define a nil node as an empty tree. A payment client silently treating a missing receiver as success would usually hide a construction bug. Receiver behavior should follow the interface's contract, not merely avoid a panic.

There is another trap with a value-receiver method reached through a nil pointer. Producing the value receiver requires dereferencing that pointer, so the call can panic before the method body can help. Our supplementary fixture checks this case separately from the guarded pointer-receiver example.

Why does a direct call compile when interface assignment fails?

Official fact: the Go specification's method-set rules distinguish a defined non-interface type T from *T. For the ordinary struct in this exercise, T has its value-receiver methods; *T has both value- and pointer-receiver methods.

type Counter struct { n int }
func (c *Counter) Inc() { c.n++ }
func (c Counter) Value() int { return c.n }

type Incrementer interface { Inc() }
type Valuer interface { Value() int }

var _ Incrementer = (*Counter)(nil)
var _ Valuer = Counter{}
var _ Valuer = (*Counter)(nil)
// var _ Incrementer = Counter{} // Does not compile.

// Inside main:
c := Counter{}
c.Inc()
var inc Incrementer = &c
inc.Inc()
fmt.Println(c.Value())

The valid example prints 2. Uncommenting the marked assignment fails because Counter lacks Inc in its method set. Yet c.Inc() succeeds. Official explanation: the Go MethodSets guide describes the addressable-call shorthand that lets this call use &c.

That convenience does not add a pointer-receiver method to the value type's method set. Interface assignment checks whether the supplied type satisfies the interface. It does not silently replace the supplied value with its address to make the assignment work.

Our recommended explanation is to name the two questions separately: “Can this expression call the method?” and “Does this type implement the interface?” The first depends partly on the expression being addressable; the second depends on the type's methods. Answering only one leaves the apparent contradiction unresolved.

A compile-time assertion such as var _ Incrementer = (*Counter)(nil) checks the type relationship. It does not instantiate a usable counter, call Inc, or prove that nil is accepted by the method. Confusing that assertion with a runtime constructor can lead to a second typed-nil bug.

An interface-state diagram distinguishing no dynamic type from a nil pointer with a concrete dynamic type, followed by the method-set check

What changes for map elements and interface-held values?

A local variable such as c is addressable. A map element containing a struct is not. In the same exercise, map[string]Counter{"a": {}}["a"].Inc() cannot use the local-variable shorthand. The Go MethodSets guide documents this distinction.

Two potential repairs express different designs. A map of *Counter values makes the pointer receiver directly available, but now pointer sharing and nil entries need a contract. Alternatively, retrieve the struct value, update the local copy, and write it back. That approach makes replacement explicit but still needs synchronization if several goroutines update the map.

Neither repair is a universal performance recommendation. In an interview, state whether clients need shared identity or independent values, then choose storage. Do not switch every map to pointers just to make one line compile.

An interface holding a Counter value also does not provide an addressable variable that can be automatically changed into *Counter. If mutation through Incrementer is required, store a pointer that actually implements it. If the consumer only needs Value, a smaller read-oriented interface can express that narrower requirement.

This is where method-set questions become design questions. Ask whether the consumer really needs mutation and whether copying the object would change identity, synchronization, or resource ownership. The smallest useful interface should reflect the operation the consumer needs, rather than every method the concrete type happens to have.

How do errors.Is and errors.As change error handling?

Use error inspection to recover meaning across contextual wrappers. Official fact: the errors package supplies Is for matching an error and As for finding a matching value through an error tree. A direct type assertion examines only the current interface value.

// Inside main, using Problem from the first example:
denied := errors.New("denied")
wrapped := fmt.Errorf("load account: %w", denied)
fmt.Println(wrapped == denied, errors.Is(wrapped, denied))

detail := fmt.Errorf("lookup: %w", &Problem{Code: "missing"})
_, direct := detail.(*Problem)
var problem *Problem
matched := errors.As(detail, &problem)
fmt.Println(direct, matched, problem.Code)

The output is false true, then false true missing. The contextual error is a different outer value, but it preserves access to the wrapped error. The original type assertion fails because the outer value is not itself a *Problem.

Notice the extra address in errors.As(detail, &problem). The function needs somewhere to store the matched *Problem; problem is that destination variable, and &problem is its address. This example uses a pointer to a pointer intentionally. It is not a reason to add more pointer levels until a compiler error disappears.

Version note: the current errors documentation also lists errors.AsType, added in Go 1.26, and recommends it for most uses. On a supported version, problem, ok := errors.AsType[*Problem](detail) returns the matched value directly. The older As form remains useful to recognize in existing code; know the target toolchain before rewriting it.

Do not compare error strings to decide whether an operation is retryable. Our example's context string could change while its underlying failure category remains the same. Define which sentinel, type, or documented classification the caller can rely on, and keep presentation wording separate from control flow.

When does wrapping expose too much of an implementation?

Official guidance: Working with Errors in Go 1.13 explains that wrapping can expose an underlying error as part of an API contract. Choosing %w lets callers inspect the cause; formatting text alone does not create the same wrapping relationship.

Suppose a repository abstraction currently uses one database driver. If callers begin matching that driver's errors, replacing the driver may change behavior even when the repository's visible method signatures stay the same. Our design recommendation is to expose the error meaning the abstraction intends to support, such as a documented not-found condition, rather than every incidental implementation detail.

That does not mean hiding all causes. Diagnostic context can be valuable, and a public contract may deliberately preserve a lower-level error. Explain who consumes the error, what recovery decision they need, and whether that dependency is acceptable.

Also avoid assuming that an error always has one linear chain. Modern wrappers can expose multiple children. The errors package traverses an error tree, so hand-written loops that repeatedly call only the single-error Unwrap helper are not a substitute for the matching APIs in every case.

What should a complete interview check include?

Our executable checks cover the successful examples and two intentional compile failures: assigning Counter to Incrementer, and calling its pointer method on a struct-valued map element. A runtime check also confirms that a nil pointer invoking a value-receiver method panics, while the guarded pointer method works.

For a production review, add the success path first. A suite that tests only non-nil concrete failures can miss the typed-nil return bug entirely. Then test a wrapped sentinel, a wrapped typed error, and an unrelated error that must not match. Check behavior through the interface used by callers, not only by invoking concrete methods directly.

These verified PracHub questions provide Go discussion or transferable interface and error-design practice. General design exercises are not presented as Go-specific prompts; apply the language rules from this article when implementing your answer.

PracHub practice questionWhat to demonstrate
Explain sync/async, Go concurrency, SQL vs NoSQLState the Go interface and error boundary around a worker
Implement custom iterators and interfacesMatch value and pointer method sets to the consumer
Design a service aggregator with robust error handlingPreserve useful failure categories through context
Design input validation and error handlingReturn a genuine nil error on success
Design a generic key-value storeSeparate a public error contract from storage details

Start with Design a service aggregator with robust error handling. Define success, absent results, and upstream failures before writing wrappers, then prove that callers can distinguish them without parsing strings.

Sources and Further Reading


Comments (0)