Wenn du nur einen Go-Stolperer kennst, sollte es dieser sein. Eine Funktion gibt einen *MyError-Pointer zurück, der nil ist. Der Aufrufer prüft if err != nil — die Bedingung ist wahr, obwohl gar kein Fehler vorliegt. Das ist kein Bug im Compiler, kein Spezialfall der Runtime, sondern eine direkte Konsequenz daraus, wie Interfaces intern dargestellt werden: als Tupel aus dynamischem Typ und dynamischem Wert. Ein nil-Pointer hat einen Typ — und genau dieser Typ macht das umhüllende Interface nicht-nil. Die Go-FAQ widmet diesem Phänomen einen eigenen Abschnitt („Why is my nil error value not equal to nil?"), und dieser Artikel arbeitet ihn von Grund auf durch: iface-Header, Reproduktion, Lösungswege, Praxis-Refactoring und die statischen Analyzer, die dich davor schützen.
Der iface-Header — Interface intern
Ein Interface-Wert ist in Go kein simpler Pointer. Die Runtime speichert ihn als Tupel aus zwei Slots — historisch in der Quellcode-Struktur iface (für Interfaces mit Methoden) bzw. eface (für any/interface{}):
┌─────────────────────────────────────┐
│ Interface-Wert (iface-Header) │
├──────────────┬──────────────────────┤
│ T (Typ) │ V (Wert) │
│ *itab │ unsafe.Pointer │
└──────────────┴──────────────────────┘
│ │
│ └── zeigt auf den
│ konkreten Wert
│
└── beschreibt den dynamischen Typ
+ die Methoden-TabelleDie Go-FAQ formuliert es so:
Under the covers, interfaces are implemented as two elements, a type T and a value V. V is a concrete value such as an int, struct or pointer, never an interface itself, and has type T.
Beide Slots zusammen bilden den dynamischen Inhalt des Interface-Werts. Der statische Typ — das, was in der Variablen-Deklaration steht (var err error, var w io.Writer) — wird vom Compiler verwaltet und ist zur Laufzeit nicht direkt sichtbar. Was zur Laufzeit zählt, ist (T, V).
Diese Zwei-Wort-Repräsentation ist kein Implementations-Detail, das man ignorieren könnte — sie ist die Erklärung für praktisch jede „komische" Interface-Beobachtung in Go.
Wann ist ein Interface nil?
Die FAQ-Regel in einem Satz:
An interface value is nil only if the V and T are both unset.
Beide Slots müssen leer sein. Nicht einer. Nicht der eine wichtiger als der andere. Beide.
package main
import "fmt"
func main() {
// Fall 1: uninitialisiertes Interface — (T=nil, V=nil) → nil
var a error
fmt.Println("Fall 1:", a == nil) // true
// Fall 2: explizit auf nil gesetzt — (T=nil, V=nil) → nil
var b error = nil
fmt.Println("Fall 2:", b == nil) // true
// Fall 3: typisierter nil-Pointer im Interface
// — (T=*MyErr, V=nil) → NICHT nil
type MyErr struct{}
var p *MyErr = nil
var c error = p
fmt.Println("Fall 3:", c == nil) // false ← die Falle
}Fall 1: true
Fall 2: true
Fall 3: falseIn Fall 3 ist der dynamische Wert V = nil — der Pointer zeigt auf nichts. Aber der dynamische Typ ist gesetzt: T = *MyErr. Damit ist die Bedingung „beide unset" verletzt, und das Interface ist nicht-nil. Der Vergleich c == nil ist false.
Das ist die ganze Falle. Alle weiteren Beispiele in diesem Artikel sind nur Variationen dieses einen Musters.
Der klassische Bug — typisierte nil-Error-Returns
Genau dieses Muster taucht in echtem Code ständig auf — meistens in selbstgeschriebenen Error-Typen. Ein Entwickler definiert einen eigenen *ValidationError, bekommt ein „Methode Error() muss Pointer-Receiver haben", und gibt den Pointer aus seiner Funktion zurück. Solange ein Fehler vorliegt, wird der Pointer initialisiert. Liegt keiner vor, bleibt er nil. Aber die Funktion-Signatur ist error — nicht *ValidationError. Und genau in diesem Übergang passiert der Bug:
package main
import "fmt"
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
// BUG — gibt *ValidationError zurück, nicht error.
// Bei „kein Fehler" bleibt err nil, wird aber typisiert!
func validate(name string) error {
var err *ValidationError // ← typisierter nil-Pointer
if name == "" {
err = &ValidationError{Field: "name", Msg: "darf nicht leer sein"}
}
return err // bei kein Fehler: (T=*ValidationError, V=nil)
}
func main() {
if err := validate("Alice"); err != nil {
fmt.Println("FEHLER:", err) // wird AUSGEFÜHRT, obwohl nichts schiefging
} else {
fmt.Println("alles okay")
}
}FEHLER: <nil>Schmerzhaft präzise: Der Aufrufer prüft err != nil, geht in den Fehler-Pfad und druckt <nil> — weil err.Error() einen nil-Pointer dereferenzieren würde, aber fmt die Panic abfängt und statt der Nachricht den Text <nil> ausgibt. In Produktion landet das in Logs, Sentry-Reports, Monitoring-Dashboards — als „Fehler ohne Nachricht". Wer den Code nicht gut kennt, sucht stundenlang.
Reproduktion und Trace
Damit du den Mechanismus siehst, hier dieselbe Situation Schritt für Schritt mit reflect:
package main
import (
"fmt"
"reflect"
)
type MyErr struct{ Msg string }
func (e *MyErr) Error() string { return e.Msg }
func produce(fail bool) error {
var p *MyErr
if fail {
p = &MyErr{Msg: "echter Fehler"}
}
return p // immer typisiert, auch wenn p == nil
}
func dump(label string, err error) {
v := reflect.ValueOf(err)
fmt.Printf("%s:\n", label)
fmt.Printf(" err == nil → %v\n", err == nil)
fmt.Printf(" reflect.Kind() → %v\n", v.Kind())
if v.Kind() == reflect.Ptr {
fmt.Printf(" reflect.IsNil() → %v\n", v.IsNil())
}
fmt.Printf(" dynamischer Typ T → %T\n", err)
fmt.Println()
}
func main() {
dump("kein Fehler (fail=false)", produce(false))
dump("Fehler (fail=true) ", produce(true))
}kein Fehler (fail=false):
err == nil → false
reflect.Kind() → ptr
reflect.IsNil() → true
dynamischer Typ T → *main.MyErr
Fehler (fail=true) :
err == nil → false
reflect.Kind() → ptr
reflect.IsNil() → false
dynamischer Typ T → *main.MyErrBeachte den ersten Fall: err == nil ist false, gleichzeitig ist der innere Pointer per reflect.IsNil() aber true. Das ist die Diagnose. Wenn du je ein Programm debuggst, in dem err != nil wahr ist, aber err.Error() paniciert oder <nil> druckt — das hier ist deine Ursache.
FAQ-Zitat — die offizielle Diagnose
Die Go-FAQ schreibt unter „Why is my nil error value not equal to nil?":
If we store a nil pointer of type
*intinside an interface value, the inner type will be*intregardless of the value of the pointer: (T=*int, V=nil). Such an interface value will therefore be non-nil even when the pointer value V inside is nil.
Und konkret zum Error-Pattern:
If all goes well, the function returns a nil p, so the return value is an error interface value holding (T=*MyError, V=nil). This means that if the caller compares the returned error to nil, it will always look as if there was an error even if nothing bad happened.
Die FAQ schließt mit dem Stilrat:
It's a good idea for functions that return errors always to use the
errortype in their signature rather than a concrete type such as*MyError, to help guarantee the error is created correctly.
Das ist die offizielle Empfehlung der Go-Authoren — nicht zufällig, sondern als direkte Konsequenz aus dem iface-Header-Mechanismus.
Lösung 1 — niemals typisierte nils zurückgeben
Die einfachste und idiomatischste Lösung: gib explizit nil zurück, sobald kein Fehler vorliegt. Keine lokale Pointer-Variable, die durchgereicht wird, kein Akkumulator-Pattern, das eventuell nil bleibt.
package main
import "fmt"
type ValidationError struct {
Field, Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
// FIX — Rückgabewert ist error (Interface), und der nil-Fall
// ist ein expliziter return nil.
func validate(name string) error {
if name == "" {
return &ValidationError{Field: "name", Msg: "darf nicht leer sein"}
}
return nil // ← explizites untypisiertes nil, (T=nil, V=nil)
}
func main() {
if err := validate("Alice"); err != nil {
fmt.Println("FEHLER:", err)
} else {
fmt.Println("alles okay")
}
}alles okayDas return nil ohne Typ-Angabe wird vom Compiler als error(nil) interpretiert — ein leerer iface-Header, (T=nil, V=nil). Der Aufrufer-Vergleich err != nil ist jetzt korrekt false.
Die Regel verallgemeinert sich: Funktionen, die ein Interface zurückgeben, sollten nie eine konkrete typisierte Variable als Rückgabewert durchreichen, wenn diese nil sein kann. Wenn du den Wert aus einem if/else-Zweig konstruierst, gib in jedem Zweig explizit nil oder den fertigen Wert zurück — niemals eine pre-deklarierte Variable mit konkretem Pointer-Typ.
Lösung 2 — Sentinel-Errors als Werte, nicht als Pointer
Der zweite Stil-Hebel: wenn du wiederverwendbare Fehler-Werte definierst (Sentinels wie io.EOF, sql.ErrNoRows), nutze Werte, nicht Pointer:
package mypkg
import "errors"
// GUT — Sentinel als Wert. errors.New gibt *errorString zurück,
// aber die Variable wird einmalig initialisiert und nie als nil
// zurückgegeben. Vergleich mit errors.Is funktioniert.
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrInvalidInput = errors.New("invalid input")
)
// Vergleich beim Aufrufer:
// if errors.Is(err, mypkg.ErrNotFound) { ... }errors.New liefert technisch einen *errorString — aber der Wert ist nie nil, weil er als Konstante initialisiert wird und nie überschrieben. Du gibst diesen Wert aus deinen Funktionen zurück, wenn der Fehler-Fall eintritt — sonst gibst du return nil. Damit gibt es keinen Übergang, an dem ein „typisierter nil-Pointer" entstehen könnte.
Für strukturierte Fehler mit Daten (Felder, Codes) ist das Pattern:
package mypkg
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return e.Resource + " " + e.ID + " nicht gefunden"
}
// Konstruktor gibt error zurück, nicht *NotFoundError.
// Aufrufer können niemals einen typisierten nil bekommen.
func newNotFound(resource, id string) error {
return &NotFoundError{Resource: resource, ID: id}
}
// Funktion, die Fehler produzieren KANN — gibt error zurück.
func Find(id string) (string, error) {
if id == "" {
return "", newNotFound("user", id)
}
return "Alice", nil // explizites nil
}Mit errors.As kann der Aufrufer dann gezielt auf den konkreten Typ prüfen, ohne dass du je einen typisierten nil-Pointer aus der Hand gibst:
var nfe *NotFoundError
if errors.As(err, &nfe) {
log.Printf("resource %s mit ID %s nicht gefunden", nfe.Resource, nfe.ID)
}Typed Nil in Methoden — wann es OK ist
Eine wichtige Differenzierung: typed nil ist nicht per se gefährlich. Pointer-Receiver-Methoden auf einen nil-Pointer aufzurufen ist legal — solange die Methode den Pointer nicht dereferenziert.
package main
import "fmt"
type Logger struct {
prefix string
}
// Defensive Methode — funktioniert auch auf nil-Receiver.
func (l *Logger) Log(msg string) {
if l == nil {
return // no-op
}
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
func main() {
var l *Logger // nil-Pointer
l.Log("hallo") // OK — kein Panic, kein Output
l = &Logger{prefix: "INFO"}
l.Log("hallo") // [INFO] hallo
}[INFO] halloDer Methoden-Aufruf l.Log(...) funktioniert, weil Go den Receiver als ersten Argument-Slot übergibt — der Wert nil ist ein gültiger Pointer-Wert. Erst die Dereferenzierung l.prefix würde panicen, und genau die wird durch den if l == nil-Guard verhindert.
Das ist ein legitimes Pattern, das du in der Stdlib siehst: viele Methoden auf *strings.Reader, *bytes.Buffer oder Helper-Typen tolerieren einen nil-Receiver als Default-No-Op. Im nächsten Praxis-Block bauen wir das gezielt aus.
Vergleich zweier Interfaces
Die Spec definiert Interface-Vergleich präzise: zwei Interface-Werte sind gleich, wenn beide Komponenten — dynamischer Typ und dynamischer Wert — gleich sind.
package main
import "fmt"
type A struct{ N int }
type B struct{ N int }
func main() {
var i1, i2 any
// Gleicher Typ, gleicher Wert → gleich
i1 = A{N: 1}
i2 = A{N: 1}
fmt.Println("A{1}==A{1}:", i1 == i2) // true
// Gleiche Felder, unterschiedlicher Typ → ungleich
i1 = A{N: 1}
i2 = B{N: 1}
fmt.Println("A{1}==B{1}:", i1 == i2) // false
// Beide nil → gleich
var n1, n2 any
fmt.Println("nil==nil :", n1 == n2) // true
// Typed nil → nicht gleich nil
var p *A
i1 = p
fmt.Println("(*A)nil==nil:", i1 == nil) // false
}A{1}==A{1}: true
A{1}==B{1}: false
nil==nil : true
(*A)nil==nil: falseAchtung: wenn der dynamische Typ nicht-vergleichbar ist (Slice, Map, Funktion), paniciert der Vergleich zur Laufzeit:
var i1, i2 any
i1 = []int{1, 2, 3}
i2 = []int{1, 2, 3}
_ = i1 == i2 // runtime panic: comparing uncomparable type []intDas ist relevant, wenn du Interfaces als Map-Keys oder in ==-Checks benutzt. Im Zweifel: reflect.DeepEqual für tiefe Wertegleichheit, oder explizit auf den konkreten Typ casten.
Praxis 1 — Refactoring eines *MyError-Patterns
Ein realistisches Vorher/Nachher. Stell dir einen HTTP-Handler-Stack vor, in dem parseRequest einen eigenen Fehler-Typ benutzt:
// VORHER — buggy
package handler
import (
"encoding/json"
"net/http"
)
type ParseError struct {
Field string
Reason string
}
func (e *ParseError) Error() string {
return e.Field + ": " + e.Reason
}
type CreateUserReq struct {
Name string `json:"name"`
Email string `json:"email"`
}
// BUG 1: lokales `var err *ParseError` bleibt eventuell nil
// ABER Funktion gibt *ParseError zurück, kein error-Interface
// → solange die Signatur so bleibt, ist es kein Typed-Nil-Bug,
// beim Refactor auf `error` aber sofort kaputt.
func parseRequest(r *http.Request) (CreateUserReq, *ParseError) {
var req CreateUserReq
var err *ParseError
if decErr := json.NewDecoder(r.Body).Decode(&req); decErr != nil {
err = &ParseError{Field: "_body", Reason: decErr.Error()}
}
if err == nil && req.Name == "" {
err = &ParseError{Field: "name", Reason: "darf nicht leer sein"}
}
return req, err
}Der Code funktioniert in dieser Form noch — solange parseRequest den konkreten Typ *ParseError zurückgibt, ist der nil-Vergleich beim Aufrufer auf den Pointer-Typ und korrekt. Sobald aber jemand die Signatur ändert (Refactoring, Interface-Anforderung, Middleware-Chain), kippt das Verhalten.
Die idiomatische Variante schließt diese Möglichkeit von Anfang an aus:
// NACHHER — robust
package handler
import (
"encoding/json"
"net/http"
)
type ParseError struct {
Field, Reason string
}
func (e *ParseError) Error() string {
return e.Field + ": " + e.Reason
}
// FIX — Signatur error von Anfang an, return nil explizit,
// keine pre-deklarierte typisierte Variable.
func parseRequest(r *http.Request) (CreateUserReq, error) {
var req CreateUserReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return req, &ParseError{Field: "_body", Reason: err.Error()}
}
if req.Name == "" {
return req, &ParseError{Field: "name", Reason: "darf nicht leer sein"}
}
return req, nil // ← explizites Interface-nil
}
func Handle(w http.ResponseWriter, r *http.Request) {
req, err := parseRequest(r)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
_ = req
}Die Regeln, die diesen Refactor leitet:
- Rückgabe-Typ ist immer das Interface (
error), nie der konkrete Typ. - Jeder Pfad gibt entweder explizit
niloder einen konstruierten Wert zurück. Kein „akkumulierter" Pointer, der durchgereicht wird. - Keine pre-deklarierte Variable vom konkreten Typ, die als Rückgabewert verwendet werden könnte.
Praxis 2 — Logger-Wrapper mit typed-nil als Default-No-Op
Hier nutzen wir typed-nil bewusst. Ein Logger soll optional sein — wer kein Logger-Objekt übergibt, soll keine Panics und keine Bedingungen im Aufrufer-Code haben. Der typed-nil-Receiver wird zum Null-Object-Pattern:
package main
import (
"fmt"
"io"
"os"
)
type Logger struct {
out io.Writer
prefix string
}
func NewLogger(out io.Writer, prefix string) *Logger {
return &Logger{out: out, prefix: prefix}
}
// Alle Log-Methoden tolerieren nil-Receiver → No-Op.
func (l *Logger) Infof(format string, args ...any) {
if l == nil {
return
}
fmt.Fprintf(l.out, "[%s INFO] ", l.prefix)
fmt.Fprintf(l.out, format+"\n", args...)
}
func (l *Logger) Errorf(format string, args ...any) {
if l == nil {
return
}
fmt.Fprintf(l.out, "[%s ERR ] ", l.prefix)
fmt.Fprintf(l.out, format+"\n", args...)
}
// Service-Code muss keinen Logger-Check machen.
type UserService struct {
log *Logger // darf nil sein
}
func (s *UserService) Create(name string) {
s.log.Infof("creating user %q", name) // auch bei s.log == nil sicher
// ... echte Logik ...
s.log.Infof("user %q created", name)
}
func main() {
// Mit Logger
svc1 := &UserService{log: NewLogger(os.Stdout, "app")}
svc1.Create("Alice")
fmt.Println("---")
// Ohne Logger — kein Crash, keine Output, kein Boilerplate beim Aufrufer
svc2 := &UserService{log: nil}
svc2.Create("Bob")
}[app INFO] creating user "Alice"
[app INFO] user "Alice" created
---Beachte den Unterschied zum Error-Fall: hier ist *Logger als konkreter Typ erwartet — keine Interface-Hülle dazwischen. Der Aufrufer hält direkt einen *Logger-Pointer, der entweder echt oder nil ist, und Go führt die Methoden-Dispatch auf dem nil-Pointer korrekt durch. Wenn *Logger durch ein Interface ginge (type LogIface interface { Infof(...); Errorf(...) }), würde der typed-nil-Wrap im Interface wieder zur Falle — und ein Aufrufer-Check if log != nil würde fälschlicherweise zutreffen.
Faustregel für dieses Pattern: nil-Receiver-Methoden sind in Ordnung, solange der Nutzer den konkreten Pointer-Typ in der Hand hält und nicht durch ein Interface darauf zugreift. Beim Wechsel auf ein Interface die typed-nil-Falle prüfen.
Tools — statische Analyzer, die dich schützen
Manuelle Disziplin ist gut, aber Compiler-Hilfen sind besser. Mehrere statische Analyzer fangen genau diese Bug-Klasse:
# golangci-lint — der Sammelpunkt für nahezu alle Go-Linter
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# errcheck — prüft, dass error-Returns nicht ignoriert werden
go install github.com/kisielk/errcheck@latest
# nilness — Teil der golang.org/x/tools — findet nil-Dereferenzierungen
go install golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latestKonfiguration für golangci-lint:
# .golangci.yml
linters:
enable:
- errcheck # vergessene error-Checks
- nilerr # return err nach if err != nil { return nil }
- nilnil # gleichzeitige Rückgabe von nil-Pointer + nil-error
- govet # enthält nilness und shadow
- staticcheck # SA4023 erkennt typed-nil-Vergleiche
- errorlint # findet error-Vergleiche, die errors.Is/As brauchenDie für unseren Fall relevanten Checks:
staticcheck SA4023— „impossible comparison of interface value with untyped nil". Erkennt Stellen, an denen ein typisierter Pointer in ein Interface gesteckt und dann mitnilverglichen wird.govet nilness— Daten-Fluss-Analyse, die nil-Werte durch das Programm verfolgt und meldet, wenn ein Pointer „immer nil" oder „immer nicht-nil" ist. Findet sowohl die Typed-Nil-Falle als auch nil-Dereferenzierungen.nilerr— meldet Funktionen, die nachif err != nileinreturn nilmachen — typischer Copy-Paste-Bug, der in derselben Familie liegt.nilnil— meldetreturn nil, nilaus(T, error)-Signaturen, wo der Caller eigentlich entweder Wert oder Fehler erwartet.
Aktiviere diese Checks in deiner CI von Anfang an. Sie kosten ein paar Sekunden pro Build und sparen dir die nächste Stunde Debugger-Sitzung.
Häufige Stolperfallen
Funktion gibt konkreten Pointer-Typ zurück, Signatur ist trotzdem `error`.
Der häufigste Auslöser. func f() error { var p *MyErr; return p } produziert immer einen nicht-nil error-Wert. Regel: niemals eine typisierte Pointer-Variable durchreichen, die nil sein kann. Stattdessen pro Pfad explizit return nil oder return &MyErr{...}.
`nil`-Vergleich vor und nach Interface-Wrap unterscheidet sich.
Solange du einen konkreten *MyErr in der Hand hältst, ist p == nil der „echte" Nil-Check des Pointers. Sobald du den Wert einer Variable vom Typ error zuweist, vergleicht err == nil den iface-Header — und der ist nicht-nil. Beim Refactoring auf Interface-Signaturen immer prüfen, ob irgendwo ein nil-Pointer eingewickelt werden könnte.
`if err != nil { return err }` mit konkretem Pointer in error-Funktion.
Wenn err ein *MyErr ist und nil sein kann, dann ist return err aus einer error-Funktion ein Typed-Nil-Return. Korrekt: if err == nil { return nil }; return err — oder besser direkt mit error arbeiten, kein *MyErr als lokale Variable.
fmt.Println(err) druckt `` bei typed nil — verwirrt das Debugging.
Der fmt-Reflection-Code erkennt typisierte nils und gibt <nil> aus, statt zu panicen. Das sieht in Logs wie „kein Fehler" aus, obwohl err != nil true ist. Bei verdächtigem Verhalten zusätzlich fmt.Printf("%T %v\n", err, err) ausgeben — der Typ-Slot zeigt sofort, was wirklich drinsteckt.
Sentinel-Errors mit `errors.New` statt eigenen Pointer-Typ-Konstanten.
var ErrNotFound = errors.New("not found") ist ein vorgegebener Wert, der nie nil ist und nie unbeabsichtigt zugewiesen wird. Wer dagegen var ErrNotFound *NotFoundError deklariert und später bei Bedarf befüllt, baut sich ein Typed-Nil-Risiko ein.
Interface-Felder in Structs — Zero Value ist nil und korrekt nil.
type Service struct { Log Logger } mit Logger als Interface — der Zero Value ist nil, ein iface-Header (T=nil, V=nil). Das ist kein Typed-Nil. Erst wenn jemand s.Log = (*ConsoleLogger)(nil) macht, kippt es. Im Konstruktor explizit prüfen oder direkt einen konkreten Default zuweisen.
Pointer-Receiver-Methoden auf nil-Receiver sind erlaubt — solange nicht dereferenziert wird.
func (l *Logger) Log(msg string) darf mit var l *Logger; l.Log(...) aufgerufen werden. Erst l.prefix oder l.out paniciert. Das nutzt das Null-Object-Pattern aus — aber nur bei direkten Pointer-Aufrufen, nicht über Interfaces.
`reflect.ValueOf(err).IsNil()` ist der Diagnose-Helfer.
Wenn du im Debugger oder im Test feststellst, dass err != nil true ist und err.Error() paniciert oder leer ist, prüfe reflect.ValueOf(err).Kind() == reflect.Ptr && reflect.ValueOf(err).IsNil(). Trifft beides zu, hast du einen typed-nil-Bug aufgespürt.
Weiterführende Ressourcen
Externe Quellen
- Go FAQ — Why is my nil error value not equal to nil?
- Go Language Specification — Interface types
- Go Language Specification — Comparison operators (Interfaces)
- The Laws of Reflection — interface representation
- Effective Go — Errors
- staticcheck SA4023 — impossible comparison with nil
- golang.org/x/tools — nilness analyzer
- golangci-lint — Linters
Verwandte Artikel
- Interfaces — Übersicht und didaktische Einführung
- Implizite Implementierung — Duck Typing in Go
- Interface Satisfaction — Method-Sets und Pointer-Receiver
- Empty Interface —
anyundinterface{} - Type Assertion und Type Switch — sichere Cast-Patterns
- Accept Interfaces, Return Structs — API-Design-Idiom
- nil-Pointer — Zero Value und Defensive Programming
- Pointer vs. Wert — Wann
*T, wannT