Let's dive deep into interfaces in Go, one of Goβs most powerful and flexible features.
An interface in Go is a type that defines a set of method signatures. If a type (like a struct
) implements all the methods defined in the interface, it automatically satisfies that interface β no explicit declaration needed!
An interface defines "what a type can do" instead of "what it is".
type Animal interface { Speak() string }
Any type that has a Speak() string
method satisfies this interface.
type Animal interface { Speak() string } type Dog struct{} type Cat struct{} func (d Dog) Speak() string { return "Woof!" } func (c Cat) Speak() string { return "Meow!" }
func makeItSpeak(a Animal) { fmt.Println(a.Speak()) }
Now we can pass either Dog
or Cat
to makeItSpeak()
β polymorphism.
makeItSpeak(Dog{}) // Woof! makeItSpeak(Cat{}) // Meow!
You donβt declare that a type implements an interface. If it matches the method signatures β it does.
var a Animal a = Dog{} // Works if Dog has Speak()
You can:
- Pass them as function arguments
- Store them in slices
- Use them as return types
animals := []Animal{Dog{}, Cat{}} for _, animal := range animals { fmt.Println(animal.Speak()) }
This is the universal type in Go, like any
.
func printAnything(i interface{}) { fmt.Println(i) }
But it loses type safety unless you do type assertion or type switch.
If you have an interface and want to access the concrete value/type, use type assertion:
var a Animal = Dog{} dog := a.(Dog) // Asserts a is of type Dog fmt.Println(dog.Speak())
To avoid a panic, use the safe form:
if dog, ok := a.(Dog); ok { fmt.Println("Dog says:", dog.Speak()) }
A cleaner way to inspect the actual type inside an interface:
func identify(a Animal) { switch v := a.(type) { case Dog: fmt.Println("It's a dog!", v.Speak()) case Cat: fmt.Println("It's a cat!", v.Speak()) default: fmt.Println("Unknown animal") } }
type Logger interface { Log(message string) } type ConsoleLogger struct{} func (ConsoleLogger) Log(message string) { fmt.Println("[Console]", message) } type FileLogger struct{} func (FileLogger) Log(message string) { // pretend we're writing to a file fmt.Println("[File]", message) }
Now write a function that takes a Logger
:
func process(l Logger) { l.Log("Processing started...") }
Pass either logger:
process(ConsoleLogger{}) process(FileLogger{})
Interfaces can include other interfaces:
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type ReadWriter interface { Reader Writer }
Now, any type that implements both Read
and Write
satisfies ReadWriter
.
Goβs standard library is filled with interfaces:
io.Reader
,io.Writer
fmt.Stringer
error
interfacehttp.Handler
in web development
type Stringer interface { String() string } func (n Note) String() string { return fmt.Sprintf("Note: %s", n.Title) }
Then you can:
fmt.Println(noteObj) // Uses String() internally
Concept | Meaning |
---|---|
Interface | A type defining a method set |
Implicit Implementation | Types satisfy interfaces by implementing methods |
Polymorphism | Interface allows multiple types to be handled with common methods |
interface{} | Universal type that accepts any value |
Type Assertion | Access the underlying type in an interface |
Type Switch | Switch based on the actual type in the interface |
Composition | Interfaces can embed other interfaces |
- To allow extensibility and abstraction
- When different types share behavior
- When building testable, loosely coupled code (e.g., mocking interfaces in tests)
Now.. Let's break down the relationship between interfaces and structs in Go in a deep, clear, and practical way.
A struct is a blueprint for creating custom data types that group together fields (data) β similar to objects or records.
type Dog struct { Name string }
An interface defines a set of method signatures. It doesnβt care how the behavior is implemented β just that it exists.
type Animal interface { Speak() string }
If a struct defines all the methods listed in an interface, it implicitly satisfies that interface.
The struct provides the data + method implementations, The interface defines the behavior contract.
type Animal interface { Speak() string } type Dog struct { Name string } func (d Dog) Speak() string { return d.Name + " says Woof!" }
Here:
Dog
is a struct.Animal
is an interface.- Since
Dog
has aSpeak()
method matching theAnimal
interface, it satisfies the interface automatically.
var a Animal a = Dog{Name: "Rocky"} fmt.Println(a.Speak()) // Rocky says Woof!
a
is of typeAnimal
(interface).- Internally,
a
stores aDog
value. - When we call
a.Speak()
, Go uses dynamic dispatch to callDog.Speak()
.
Concept | Struct | Interface |
---|---|---|
What it defines | Fields (data) and optionally methods | Only method signatures (no fields) |
Purpose | Concrete implementation | Abstract behavior contract |
How they connect | Implements methods | Requires those methods |
Declaration | type Person struct {...} | type Printer interface {...} |
Explicit link? | β No keywords like implements | β Auto-checks by method match |
- Interfaces let us abstract over structs.
- We can write generic functions that work with any type that satisfies the interface.
- Structs keep our logic modular by implementing specific behaviors.
- π§± Struct = Appliance (like a Fan or AC)
- π Interface = Power socket (expects a plug that fits)
- If the appliance (struct) has the right plug (method), it fits the socket (interface) and works!
type Cat struct { Name string } func (c Cat) Speak() string { return c.Name + " says Meow!" } animals := []Animal{ Dog{Name: "Bruno"}, Cat{Name: "Kitty"}, } for _, animal := range animals { fmt.Println(animal.Speak()) }
This is polymorphism β multiple types behave similarly via interface.
- Structs = Concrete data + logic (methods)
- Interfaces = Behavior contract (method signatures)
- A struct implements an interface by defining all its methods
- The relationship is implicit, flexible, and powerful
- Interfaces enable polymorphism, abstraction, and testability
Now.. Letβs go deep into Generics in Go, introduced in Go 1.18, which added parametric polymorphism β a big milestone for the language.
Generics allow us to write functions, methods, and types that work with any data type, while retaining type safety.
This is similar to TypeScriptβs generics, C++ templates, or Javaβs generics.
Before generics, we had to:
-
Repeat code for different types:
func sumInts(nums []int) int { ... } func sumFloats(nums []float64) float64 { ... }
-
Or use
interface{}
(not type-safe):func printAll(values []interface{}) { for _, v := range values { fmt.Println(v) } }
Now, with generics, we can write:
func sum[T int | float64](nums []T) T
π― One version of the function works for multiple types with full type safety.
func PrintSlice[T any](s []T) { for _, v := range s { fmt.Println(v) } }
T
is the type parameterany
is a constraint (alias forinterface{}
)s []T
means a slice of some typeT
T
can be anything βint
,string
,struct
, etc.
β Usage:
PrintSlice([]int{1, 2, 3}) PrintSlice([]string{"Go", "Rust", "JS"})
Constraints limit what types a generic can accept.
type Number interface { int | float64 }
Then:
func Sum[T Number](nums []T) T { var total T for _, v := range nums { total += v } return total }
β
Now Sum
works for both int
and float64
arrays.
Want to use ==
, !=
, or as map keys?
func FindIndex[T comparable](slice []T, target T) int { for i, v := range slice { if v == target { return i } } return -1 }
βοΈ comparable
allows ==
comparison β required for map keys.
We can use generics in structs too!
type Box[T any] struct { value T } func (b Box[T]) Get() T { return b.value }
β Usage:
intBox := Box[int]{value: 42} fmt.Println(intBox.Get()) // 42 strBox := Box[string]{value: "Go!"} fmt.Println(strBox.Get()) // Go!
You can use more than one type parameter:
type Pair[K, V any] struct { key K value V }
p := Pair[string, int]{key: "age", value: 30}
A method on a generic type also gets the type param:
func (p Pair[K, V]) Display() { fmt.Printf("Key: %v, Value: %v\n", p.key, p.value) }
Letβs say we want to allow only types that have an Area()
method.
type Shaper interface { Area() float64 } func PrintArea[T Shaper](s T) { fmt.Println("Area:", s.Area()) }
Any struct that implements Area()
satisfies the constraint.
You can define your own constraints:
type SignedNumber interface { int | int32 | int64 }
Then:
func AddAll[T SignedNumber](nums []T) T { var sum T for _, v := range nums { sum += v } return sum }
func Map[T any, U any](input []T, f func(T) U) []U { result := make([]U, len(input)) for i, v := range input { result[i] = f(v) } return result }
Usage:
squares := Map([]int{1, 2, 3}, func(n int) int { return n * n }) fmt.Println(squares) // [1 4 9]
Feature | Description | |
---|---|---|
T any | T is a generic type; any means any type | |
Constraints | Limit what types can be used with T | |
comparable constraint | Restrict to types supporting == , != | |
Custom constraints | Like `type Number interface { int | float64 }` |
Generics in structs | Define reusable and type-safe data structures | |
Generic functions/methods | Allow flexible and reusable logic | |
Full type safety | Catch errors at compile-time | |
Replaces interface{} hacks | No need for reflection or type assertions |
- Reusable utilities (like
Map
,Filter
,Reduce
) - Collections (
Stack[T]
,Queue[T]
) - Algorithms (
Min
,Max
,Search
) - Service layers in backend apps
- Type-safe helper packages