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.Writerfmt.Stringererrorinterfacehttp.Handlerin 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:
Dogis a struct.Animalis an interface.- Since
Doghas aSpeak()method matching theAnimalinterface, it satisfies the interface automatically.
var a Animal
a = Dog{Name: "Rocky"}
fmt.Println(a.Speak()) // Rocky says Woof!ais of typeAnimal(interface).- Internally,
astores aDogvalue. - 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)
}
}Tis the type parameteranyis a constraint (alias forinterface{})s []Tmeans a slice of some typeTTcan 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