Skip to main content

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.

Go Interface Structure

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

type Abser interface {
    Abs() float64
}

Implicit Implementation

A type implements an interface by implementing its methods. There is no explicit declaration of intent, no “implements” keyword.
type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    var a Abser
    f := MyFloat(-math.Sqrt2)
    v := Vertex{3, 4}

    a = f  // a MyFloat implements Abser
    a = &v // a *Vertex implements Abser

    // a = v // Error! Vertex does not implement Abser (Abs method has pointer receiver)

    fmt.Println(a.Abs())
}

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.
Go Interface Structure Dynamic Dispatch: When you call a method on an interface value, Go looks up the method in the itab and calls it on the data pointer. This is how polymorphism works in Go.
var a Abser = MyFloat(3.14)
// a.itab points to the method table for MyFloat
// a.data points to the value 3.14
a.Abs() // Go looks up Abs in MyFloat's method table and calls it

Interface Values with Nil Underlying Values

This is a common source of confusion. An interface can be in three states:
  1. Nil interface: Both type and value are nil (nil, nil)
  2. Non-nil interface with nil value: Type is set, value is nil (*T, nil)
  3. Non-nil interface with non-nil value: Both are set (*T, &value)
var i interface{}
fmt.Println(i == nil) // true: (nil, nil)

var p *int
i = p
fmt.Println(i == nil) // false! (*int, nil) - type is set
fmt.Println(i.(*int) == nil) // true - the underlying value is nil
Why This Matters: A method can be called on a nil interface value if the interface is non-nil (state 2). This is why Go methods often check for nil receivers:
func (v *Vertex) Abs() float64 {
    if v == nil {
        return 0
    }
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

The Empty Interface

The interface type that specifies zero methods is known as the empty interface:
interface{}
Since Go 1.18, any is an alias for interface{} and is preferred in new code:
// These are identical:
func old(v interface{}) {}
func modern(v any) {}
An empty interface may hold values of any type. (Every type implements at least zero methods.) Empty interfaces are used by code that handles values of unknown type. For example, fmt.Print takes any number of arguments of type interface{}.
Pitfall — Overusing Empty Interface: Reaching for interface{} (or any) is often a sign that you should use generics (Go 1.18+) or a more specific interface instead. Empty interfaces abandon type safety — you lose compiler checks and push errors to runtime. Use them for truly dynamic cases (JSON parsing, logging), but prefer concrete types or type-parameterized functions for everything else.
func describe(i interface{}) {
    fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
    var i interface{}
    describe(i)

    i = 42
    describe(i)

    i = "hello"
    describe(i)
}

Type Assertions

A type assertion provides access to an interface value’s underlying concrete value.
t := i.(T)
This statement asserts that the interface 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.
t, ok := i.(T)
If 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.
func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok) // 0 false

    // f = i.(float64) // panic
}

Type Switches

A type switch is a construct that permits several type assertions in series.
func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
}

Idiomatic Interface Design

Go’s standard library demonstrates a powerful principle: define small interfaces, accept interfaces, return structs.
// Good: Small, focused interfaces (from the standard library)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Compose small interfaces into larger ones as needed
type ReadWriter interface {
    Reader
    Writer
}
The Go Proverb: “The bigger the interface, the weaker the abstraction.” Interfaces with one or two methods are the sweet spot in Go. The standard library’s io.Reader, io.Writer, fmt.Stringer, and error interfaces each have exactly one method, and they are among the most powerful abstractions in the language.When designing your own interfaces, start from the consumer side: what methods does the calling code actually need? Define the interface there, not on the implementing side. This is the opposite of Java-style “implement interface on the class” thinking.

Interview Deep-Dive

Strong Answer:
  • Go interfaces are satisfied implicitly: a type implements an interface if it has all the methods the interface requires. There is no implements keyword. Under the hood, an interface value is a two-word pair: an itab pointer (which contains the concrete type information and a method dispatch table) and a data pointer (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 Closer with a Close() error method. A type in an imported package happens to have a Close() error method 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.
Follow-up: Explain the nil interface gotcha. Why does an interface holding a nil pointer not equal nil?An interface is in one of three states: completely nil (both type and value are nil), non-nil with a nil value (type is set, value is nil), or fully non-nil. The == 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.
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.Reader instead 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 a strings.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.Reader but the underlying type is *os.File, the caller cannot call Stat() or Seek() 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 a Database interface). 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.
**Follow-up: The Go proverb says ‘the bigger the interface, the weaker the abstraction.’ Explain why, using a concrete example.”>A large interface (say, 10 methods) couples the consumer tightly to a specific implementation shape. The more methods an interface requires, the fewer types can satisfy it, which defeats the purpose of abstraction. Consider a 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.
Strong Answer:
  • In Go, a value of type T can 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 of T. The method set of *T includes both pointer-receiver and value-receiver methods. So if you have func (v *Vertex) Scale(f float64), a Vertex value cannot satisfy an interface that requires Scale, but a *Vertex can.
  • The reason is fundamental to how interfaces work. An interface holds a copy of the value. If you store a Vertex value 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 original Vertex to 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 *Vertex in the interface instead of a Vertex. 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.
Follow-up: Can you call a method on a nil interface value? What about a non-nil interface holding a nil pointer?Calling a method on a nil interface (both type and data are nil) causes a nil pointer dereference panic — there is no method table to look up. But calling a method on a non-nil interface holding a nil pointer works fine — the method is dispatched through the itab, and the nil pointer is passed as the receiver. This means your method must handle a nil receiver gracefully: 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.