Accept Interfaces, Return Structs - Idiomatic Go
golang's important design principle for writing Idiomatic Go
If you write Go, you will hear this advice again and again:
“Accept interfaces, return structs.”
At first, it sounds confusing.
Let’s break it down slowly and simply—with real examples.
First: What Does It Mean?
✅ Accept interfaces
➡️ Functions should take interfaces as input
type Store interface {
Save(data string) error
}
func ProcessData(s Store) error {
return s.Save("hello")
}✅ Return structs
➡️ Functions should return concrete types (structs)
type PostgresStore struct {}
func NewPostgresStore() *PostgresStore {
return &PostgresStore{} // return struct here not interfaces
}
func (p *PostgresStore) Save(data string) error {
fmt.Println("saving to postgres:", data)
return nil
}
This keeps your code:
Simple
Flexible
Easy to test
Easy to understand
Why Not Return Interfaces?
Many beginners write code like this:
❌ Not recommended
func NewStore() Store {
return &PostgresStore{}
}Why is this bad?
Because:
The caller doesn’t know what they actually got
You hide useful methods
Debugging becomes harder
You add abstraction too early
Go prefers explicitness.
Correct Way: Return Structs
✅ Recommended
func NewPostgresStore() *PostgresStore {
return &PostgresStore{}
}Now the caller:
Knows exactly what type it is
Can use all methods
Can still treat it as an interface later
Where Do Interfaces Fit Then?
Interfaces are best used at the point of usage, not creation.
Example: Accept an Interface
type Store interface {
Save(data string) error
}
func ProcessData(s Store) error {
return s.Save("hello")
}This function:
Doesn’t care how data is saved
Works with any Store
Is easy to test
Full Example (Real-World Style)
Step 1: Define a Struct
type PostgresStore struct {}
func (p *PostgresStore) Save(data string) error {
fmt.Println("saving to postgres:", data)
return nil
}Step 2: Return the Struct
func NewPostgresStore() *PostgresStore {
return &PostgresStore{}
}Step 3: Accept Interface Where Needed
func ProcessData(store Store) error {
return store.Save("important data")
}Step 4: Use Them Together
store := NewPostgresStore()
ProcessData(store)Why This Is Idiomatic Go
This pattern:
Avoids unnecessary abstraction
Makes testing easy
Keeps code readable
Matches how Go’s standard library works
Example from Go itself:
func NewReader(r io.Reader) *bufio.ReaderGo returns structs and accepts interfaces everywhere.
Testing Becomes Super Easy
Because you accept interfaces, you can use mocks easily:
type MockStore struct {}
func (m *MockStore) Save(data string) error {
return nil
}func TestProcessData(t *testing.T) {
mock := &MockStore{}
err := ProcessData(mock)
if err != nil {
t.Fail()
}
}No frameworks. No magic. Just Go.
Common Mistakes to Avoid
❌ Returning interfaces from constructors
❌ Creating large interfaces
❌ Forcing interfaces everywhere
❌ Writing Java-style abstractions
In summary
Go is about clarity over cleverness.
If someone can read your function and instantly understand:
What it returns
What it needs
Then you’re writing good Go.
If you enjoyed this deep dive…
I write weekly about:
Go performance and runtime behavior
Kubernetes-native service design
Expert-level engineering lessons from real systems
Subscribe if you want more posts like this.


