• Interface defines method signatures, allowing methods to accept any type that implements them, enabling flexibility and polymorphism. For example, instead of creating a method that calculates area for each struct shape, you can have a generalized one.

    package main
    
    import (
        "fmt"
        "math"
    )
    
    // Without Interface
    type Rectangle struct {
        Width  float64
        Height float64
    }
    
    type Circle struct {
        Radius float64
    }
    
    // Separate function for Rectangle
    func CalculateRectangleArea(r Rectangle) float64 {
        return r.Width * r.Height
    }
    
    // Separate function for Circle
    func CalculateCircleArea(c Circle) float64 {
        return math.Pi * c.Radius * c.Radius
    }
    
    // With Interface (Solution)
    // Any type that has a method Area() float64 satisfies this interface automatically (no explicit declaration needed).
    type Shape interface {
        Area() float64 // a method that returns float
    }
    
    // Implement Shape interface for Rectangle
    func (r Rectangle) Area() float64 {
        return r.Width * r.Height
    }
    
    // Implement Shape interface for Circle
    func (c Circle) Area() float64 {
        return math.Pi * c.Radius * c.Radius
    }
    
    // Single function that works with any Shape
    func PrintArea(s Shape) {
        fmt.Println("Area:", s.Area())
    }
    
    func main() {
        rect := Rectangle{Width: 10, Height: 5}
        circle := Circle{Radius: 7}
    
        // Without Interface: Separate functions for each type
        fmt.Println("Without Interface:")
        fmt.Println("Rectangle Area:", CalculateRectangleArea(rect)) // Output: 50
        fmt.Println("Circle Area:", CalculateCircleArea(circle))     // Output: 153.93804002589985
    
        // With Interface: Single function for any Shape
        fmt.Println("\nWith Interface:")
        PrintArea(rect)   // Output: Area: 50
        PrintArea(circle) // Output: Area: 153.93804002589985
    }
    
    • This is a core concept in Go’s type system; instead of designing our abstractions in terms of what kind of data our types can hold, we design our abstractions in terms of what actions our types can execute.
  • An interface value is two words wide: one word points to a type's method table, the other points to the actual underlying data. Understanding this internal representation clarifies most interface confusion.

  • Use cases:

    • Abstraction: Defines what a type should do without specifying how, promoting modularity.
    • Polymorphism: Enables functions to work with multiple types that satisfy the interface.
    • Decoupling: Reduces dependencies between components, making code easier to maintain.
    • Testing: Simplifies unit testing by allowing mock implementations of dependencies.
  • Passing a value into an interface{} parameter performs a type conversion. Inside the function, the parameter's static type is interface{} — not "any type." All values have exactly one type at runtime.

    func DoSomething(v interface{}) {
      // v's type here is interface{}, not the original type
    }
    
  • Use interfaces when you want a single function to work across multiple types, like a ProcessPayment(p PaymentMethod) function that handles both CreditCard and PayPal without separate implementations for each.

    • Interface defines WHAT a type must be able to do, not HOW.
  • In Go interface resolution is implicit. If the type you pass in matches what the interface is asking for, it will compile.

  • There’s also the concept of empty interface (interface{}) which specifies zero methods, so any type satisfies it automatically — every type implements "at least zero methods." It's used when a function needs to accept values of a completely unknown type.

    • Imagine writing a Println that only accepts a string:

      func Println(format string) (n int, err error) {
          return fmt.Fprintln(os.Stdout, format)
      }
      
      • This breaks the moment you try to pass anything else, like a number:

        Println("Number: ", 10)
        // compile error: too many arguments in call to Println
        
      • Instead of writing a separate overload for every possible type, you accept interface{} (or any in modern Go) and let the runtime figure out the concrete type:

        func Println(a ...interface{}) (n int, err error) {
            return fmt.Fprintln(os.Stdout, a...)
        }
        
        func main() {
            n := 10
            s := "Hello!"
            r := []rune("hi!")
        
            Println(s, n, r) // works with any mix of types
        }
        
    • any is just an alias for interface{}, introduced in Go 1.18 to make code more readable — they are 100% interchangeable, any is simply shorter.

      // These two signatures are identical:
      func Println(a ...interface{}) (n int, err error) { ... }
      func Println(a ...any) (n int, err error) { ... }
      
      • Prefer accepting interface{}/any as a parameter over returning it. Returning interface{} pushes type assertions onto the caller and tends to bloat the returning function with type-specific logic; accepting it keeps the flexibility on the input side (Postel's Law applied to interfaces).
    • In fact, this is how the fmt package functions are implemented. fmt.Fprintln's real signature is func Fprintln(w io.Writer, a ...interface{}) (n int, err error) — that ...interface{} is what lets fmt.Println accept any number of arguments of any type.

    • Use the empty interface when a function genuinely doesn't care about a value's type (e.g. just printing it or passing it through), not as a shortcut to avoid designing a proper interface — you lose compile-time type safety and have to type-assert to get the value back.

  • A pointer receiver method requires a pointer to satisfy the interface — the value type alone won't work. A value receiver method is satisfied by both the value and the pointer.

    func (c *Cat) Speak() string { return "Meow!" }
    
    var animals []Animal
    animals = append(animals, new(Cat)) // works
    
    animals = append(animals, Cat{}) // ERROR: Cat does not implement Animal (Speak requires pointer receiver)
    
    • Rule of thumb: a pointer type can call methods defined on the value type, but not vice versa — because everything in Go is passed by value, including method receivers.
  • Tip: Design custom interfaces (like json.Unmarshaler) to let a type describe its own behavior, rather than writing one big function with type-specific branches for every case.

    type Unmarshaler interface {
        UnmarshalJSON([]byte) error
    }