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
}
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:
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.
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) { ... }
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)
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
}