Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Interfaces
Interfaces are named collections of method signatures. They are the core of polymorphism in Go. Go’s interfaces are implicitly satisfied — a type does not declare “I implement this interface.” Instead, if a type has all the methods an interface requires, it automatically satisfies that interface. Think of it like a job posting: the posting says “must know Go, SQL, and Docker.” If you know all three, you qualify — you do not need to submit a separate “I implement JobCandidate” declaration. This design enables loose coupling: a package can define an interface, and types from completely unrelated packages can satisfy it without knowing about each other.Interface Definition
Implicit Implementation
A type implements an interface by implementing its methods. There is no explicit declaration of intent, no “implements” keyword.Interface Internals
Under the hood, an interface value is represented as a pair:(type, value) or more precisely (itab, data)
Where:
- itab (interface table): Contains type information and a pointer to the method table for the concrete type.
- data: Pointer to the actual value.
Interface Values with Nil Underlying Values
This is a common source of confusion. An interface can be in three states:- Nil interface: Both type and value are nil
(nil, nil) - Non-nil interface with nil value: Type is set, value is nil
(*T, nil) - Non-nil interface with non-nil value: Both are set
(*T, &value)
The Empty Interface
The interface type that specifies zero methods is known as the empty interface:any is an alias for interface{} and is preferred in new code:
fmt.Print takes any number of arguments of type interface{}.
Type Assertions
A type assertion provides access to an interface value’s underlying concrete value.i holds the concrete type T and assigns the underlying T value to the variable t.
If i does not hold a T, the statement will trigger a panic.
To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.
i holds a T, then t will be the underlying value and ok will be true.
If not, ok will be false and t will be the zero value of type T, and no panic occurs.
Type Switches
A type switch is a construct that permits several type assertions in series.Idiomatic Interface Design
Go’s standard library demonstrates a powerful principle: define small interfaces, accept interfaces, return structs.Interview Deep-Dive
Go interfaces are satisfied implicitly. Explain how this works under the hood, and describe a scenario where implicit satisfaction causes a subtle production bug.
Go interfaces are satisfied implicitly. Explain how this works under the hood, and describe a scenario where implicit satisfaction causes a subtle production bug.
Strong Answer:
- Go interfaces are satisfied implicitly: a type implements an interface if it has all the methods the interface requires. There is no
implementskeyword. Under the hood, an interface value is a two-word pair: anitabpointer (which contains the concrete type information and a method dispatch table) and adatapointer (which points to the actual value). When you assign a concrete value to an interface variable, the compiler generates the itab for that type-interface pair and stores it alongside the data pointer. - When you call a method on an interface, the runtime looks up the method in the itab’s dispatch table and calls it with the data pointer. This is dynamic dispatch — similar to vtables in C++, but resolved at assignment time rather than object creation time.
- The subtle production bug scenario: you define an interface
Closerwith aClose() errormethod. A type in an imported package happens to have aClose() errormethod that does something completely different (like closing a database connection when you expected it to close a file). Because Go uses structural typing, this type accidentally satisfies your interface, and the code compiles and runs but does the wrong thing at runtime. This is called “accidental implementation” and is the dark side of implicit interfaces. - The defense against accidental implementation is to use interface names that are more specific than single-method names from the standard library, and to keep interfaces small and focused. The Go standard library mitigates this by using very specific method signatures —
Read([]byte) (int, error)is unlikely to be accidentally implemented with the wrong semantics.
== nil check on an interface only returns true in the first state. If you have var p *MyError = nil and assign it to an error interface, the interface now holds (*MyError, nil) — the type field is set, so the interface is non-nil even though the underlying value is nil. This means err != nil returns true, and the caller thinks there IS an error when there is not. The fix is to never return a typed nil pointer as an interface. Always return the bare nil: if myErr != nil { return myErr }; return nil. This is one of Go’s most confusing gotchas and a common interview topic because it tests deep understanding of how interfaces are represented in memory.'Accept interfaces, return structs' is a Go proverb. Explain what it means, why it matters for API design, and when you would violate it.
'Accept interfaces, return structs' is a Go proverb. Explain what it means, why it matters for API design, and when you would violate it.
Strong Answer:
- “Accept interfaces, return structs” means your function parameters should be interfaces (so callers can pass any compatible type, enabling testing and flexibility) and your return types should be concrete structs (so callers get the full type information and do not lose access to methods or fields not in the interface).
- Why accept interfaces: it decouples your function from specific implementations. If your function takes an
io.Readerinstead of*os.File, callers can pass a file, a network connection, a bytes buffer, a compressed stream, or a test mock. You gain testability for free because tests can pass astrings.NewReader("test data")instead of creating actual files. - Why return structs: returning an interface hides the concrete type from the caller, which limits what they can do. If you return
io.Readerbut the underlying type is*os.File, the caller cannot callStat()orSeek()without a type assertion. Returning the concrete type gives the caller full access, and they can still assign it to an interface variable if they want the abstraction. - When to violate it: factory functions that return different concrete types based on input should return an interface (like
NewDatabase("postgres")returning aDatabaseinterface). Also, when you want to hide implementation details of an unexported type, returning an interface is appropriate because the caller cannot name the concrete type anyway.
Repository interface with Create, Read, Update, Delete, List, Count, BeginTransaction, Migrate, and Backup methods. A mock for testing would need to implement all 10 methods even if the function under test only calls Read. Compare this to io.Reader with a single method: hundreds of types in the standard library and ecosystem satisfy it. The practical guideline is: define interfaces at the consumer, not the producer. If your function only needs to read data, accept an io.Reader, not a *os.File or a Database. If it needs to read and close, accept an io.ReadCloser. Only combine into larger interfaces when a consumer genuinely needs all the methods. This is interface segregation applied to Go.You have a type with a pointer receiver method. A colleague stores a value (not a pointer) of that type in an interface variable and wonders why it does not compile. Explain the underlying mechanics.
You have a type with a pointer receiver method. A colleague stores a value (not a pointer) of that type in an interface variable and wonders why it does not compile. Explain the underlying mechanics.
Strong Answer:
- In Go, a value of type
Tcan call methods with both value receivers and pointer receivers when called directly (the compiler automatically takes the address). But when stored in an interface, only value-receiver methods are in the method set ofT. The method set of*Tincludes both pointer-receiver and value-receiver methods. So if you havefunc (v *Vertex) Scale(f float64), aVertexvalue cannot satisfy an interface that requiresScale, but a*Vertexcan. - The reason is fundamental to how interfaces work. An interface holds a copy of the value. If you store a
Vertexvalue in an interface, the interface has its own copy. If the interface then called a pointer-receiver method, that method would modify the copy inside the interface, not the original value. This would be silently wrong behavior — the caller would expect their originalVertexto be modified but it would not be. Go prevents this at compile time rather than allowing the confusing runtime behavior. - The practical fix is straightforward: store a
*Vertexin the interface instead of aVertex. In practice, since most types use pointer receivers for all methods (following the consistency rule), most types are stored as pointers in interfaces. - This is why the receiver type consistency rule matters so much: if some methods are value receivers and others are pointer receivers, you create a confusing situation where the type partially satisfies interfaces depending on whether you use a value or pointer.
func (v *Vertex) String() string { if v == nil { return "<nil>" }; ... }. This is actually a useful pattern for implementing “null object” behavior, where a nil receiver returns sensible defaults instead of panicking.