Go Basics: Avoid Typed-Nil Error Surprises
A helper can accidentally return a non-nil error even when its pointer variable is nil. Inspect the interface’s dynamic type, then return an explicit nil on the success path.
Prepare a scratch directory
Tested on Linux amd64 with Go 1.26.10. Install Go and make go available on PATH. You also need Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. The example uses only the standard library and needs no module or external dependency.
Keep the same terminal open. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as interface-nil-demo.go:
package main
import "fmt"
type ValidationError struct{ Field string }
func (e *ValidationError) Error() string {
if e == nil {
return "nil validation pointer"
}
return "invalid field: " + e.Field
}
func accidental() error {
var detail *ValidationError
return detail
}
func validate(ok bool) error {
if ok {
return nil
}
return &ValidationError{Field: "port"}
}
func main() {
err := accidental()
fmt.Println("Typed nil equals nil:", err == nil)
fmt.Printf("Dynamic type: %T\n", err)
fmt.Println("Success equals nil:", validate(true) == nil)
fmt.Println("Failure:", validate(false))
}Run it:
go run interface-nil-demo.goObserved output:
Typed nil equals nil: false
Dynamic type: *main.ValidationError
Success equals nil: true
Failure: invalid field: portRead the result
The interface returned by accidental holds a dynamic type (*main.ValidationError) and a nil pointer value. It therefore differs from a nil interface, which has neither a dynamic type nor a value. The explicit return nil in validate produces the expected success result.
Apply it carefully
A method on a nil pointer can run, but dereferencing its receiver without a check will panic. The nil-safe Error method here makes the example inspectable; it does not make returning typed nil errors a good API design. Prefer functions returning error with explicit success and failure branches.
The official Go reference documents the behavior used here.
Clean up
Remove the saved source, return to the starting directory, and remove the empty practice directory. go run does not leave an executable here; Go manages its build cache separately.
rm -- interface-nil-demo.go
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect its remaining contents before removing anything else.
Acknowledgements
This post was created with assistance from AI.