For the longest time, I avoided interfaces.
Not because I thought they were useless.
But because every explanation I came across felt like a computer science lecture.
People kept saying things like:
"Program to interfaces."
"Accept interfaces, return structs."
I understood the words.
I just didn't understand why.
Why would I need an interface when my structs and functions were working just fine?
Then I ran into a problem that structs alone couldn't solve.
That's when interfaces stopped feeling like theory and started feeling like one of the most practical features in Go.
The Story: Dog, Cat, and a Better Way
I started with a dog.
type Dog struct {
Name string
}
func (d Dog) MakeSound() string {
return d.Name + " says Woof!"
}
Then came the cat.
type Cat struct {
Name string
}
func (c Cat) MakeSound() string {
return c.Name + " says Meow!"
}
Everything looked fine.
Until I wanted one function that could make any animal speak.
My first thought wasn't "interface."
It was...
"There has to be a better way than writing one function for every animal."
I almost started writing
PrintDogSound()
PrintCatSound()
PrintCowSound()
PrintBirdSound()
That didn't feel right.
There had to be a cleaner solution.
There was.
The Beginner Mistake
Many Go beginners postpone learning interfaces.
I certainly did.
The word sounds like something from a computer science lecture.
In reality, it's one of the simplest ideas in Go.
The trick is realizing that interfaces aren't about data.
They're about behavior.
What Is an Interface?
A struct answers one question.
What is this thing?
type Dog struct {
Name string
}
A dog has a name.
That's data.
An interface answers a different question.
What can this thing do?
type Speaker interface {
MakeSound() string
}
Notice something.
The interface doesn't mention dogs.
Or cats.
Or humans.
It only says:
"If you can make a sound, you're welcome here."
That's the magic.
No implements Keyword?
If you're coming from Java or C#, this part feels strange.
Normally you'd write something like
class Dog implements Speaker
Go doesn't do that.
Instead, it quietly checks:
"Does this type have the required method?"
If yes...
Congratulations.
It already satisfies the interface.
No keyword.
No inheritance.
No registration.
Just behavior.
The "Aha!" Moment
Let's add more types.
type Speaker interface {
MakeSound() string
}
type Dog struct {
Name string
}
func (d Dog) MakeSound() string {
return d.Name + " says Woof!"
}
type Cat struct {
Name string
}
func (c Cat) MakeSound() string {
return c.Name + " says Meow!"
}
type Human struct {
Name string
}
func (h Human) MakeSound() string {
return h.Name + " says Hello!"
}
Now watch this.
func Announce(s Speaker) {
fmt.Println(s.MakeSound())
}
func main() {
Announce(Dog{Name: "Rex"})
Announce(Cat{Name: "Milo"})
Announce(Human{Name: "Perez"})
}
The beautiful part is that Announce never asks:
- Are you a Dog?
- Are you a Cat?
- Are you a Human?
It only asks:
Can you make a sound?
If the answer is yes...
The function is happy.
That's the moment interfaces finally clicked for me.
⚠️ Beginner Mistake #1: Creating Interfaces Too Early
Once interfaces make sense, beginners often swing too far in the other direction.
Suddenly everything gets an interface.
Even when there's only one implementation.
type UserRepository interface {
GetUser(id string) User
}
If you only have one repository...
You probably don't need the interface yet.
A popular Go saying is:
Accept interfaces. Return structs.
Another one you'll hear often is:
Define interfaces where they're used, not where they're implemented.
Start simple.
Add interfaces when they solve a real problem.
Not before.
⚠️ Beginner Mistake #2: Using any for Everything
Some beginners discover
any
and think they've found the ultimate shortcut.
func Print(v any) {
fmt.Println(v)
}
Sure...
It accepts everything.
But it also throws away type safety.
Instead of saying
"I accept anything."
Ask yourself:
"What behavior do I actually need?"
That's usually an interface.
Real Examples You'll See Every Day
Once interfaces click, you'll notice they're everywhere.
io.Readerio.Writerhttp.Handler- Database repositories
- Payment gateways
- Logging libraries
They all follow the same idea.
Describe behavior.
Not implementation.
Struct vs Interface
Whenever I'm unsure, I ask myself one question:
Am I describing data... or behavior?
If it's data...
Use a struct.
If it's behavior...
Use an interface.
Simple.
| Situation | Interface? |
|---|---|
| Multiple types share behavior | ✅ |
| Only one implementation | ❌ |
| Modeling data | ❌ |
| Want loose coupling | ✅ |
| Future implementations expected | ✅ |
The Mental Model That Finally Stuck
Forget complicated definitions.
Just remember this.
Struct = What is it?
Interface = What can it do?
Structs describe things.
Interfaces describe capabilities.
That distinction is what makes Go code so flexible without becoming complicated.
Final Thoughts
One thing I love about Go is that it encourages simple designs.
You don't start with giant inheritance trees.
You don't spend hours designing interfaces "just in case."
You build concrete types.
Then, when multiple types share behavior...
You introduce an interface.
Go doesn't care what something is.
It cares what something can do.
That tiny mindset shift completely changed how I write Go.
Over to You
What Go concept took you the longest to understand?
- Interfaces?
- Pointers?
- Slices?
- Goroutines?
I'd love to hear your story in the comments.
If you enjoyed this article, consider following the Golang Beginner Mistakes series, where we break down the kinds of mistakes almost every new Go developer makes—and how to avoid them.
Top comments (0)