Start With the Struct, Not the Interface
When I start a new feature in Go I keep reaching for the interface first. That order is wrong: write the concrete struct, and let interfaces show up at whatever consumes the dependency.
Every time I start a new feature in Go I catch myself typing type AvatarService interface { before I’ve written a line that does anything. That’s a habit from other languages, where the interface is the thing you declare so a container has something to bind to. In Go the order is backwards. You write the concrete thing first, and interfaces show up later, driven by whatever consumes it rather than by the thing itself.
Here’s the order that works for me.
1. Struct and concrete methods first
Figure out what the feature actually needs to hold as state, config, dependencies, clients, and write the struct with real methods. No interface yet.
type AvatarService struct { Store S3Storage // concrete, for now}
func (a AvatarService) UploadAvatar(userID string, data []byte) error { return a.Store.Save("avatars/"+userID, data)}S3Storage is a real struct with real methods and AvatarService calls them directly. The logic runs, and I haven’t committed to an abstraction I’d have to defend later.
Writing the interface first means guessing which methods matter before the code exists to use them. Every time I’ve done that I ended up with a method nobody calls and a missing one I needed on the second day.
2. Look at what it depends on, because that’s where interfaces get born
This is the part that took me longest to get. The interface question isn’t “what can my new service do.” It’s “what do the things my service depends on need to expose.”
AvatarService depends on storage, so Storage becomes an interface. Not because S3Storage needs one, but because AvatarService needs to swap or mock its own dependency.
type Storage interface { Save(key string, data []byte) error Get(key string) ([]byte, error)}So flip the mental model: interfaces belong to the dependency being consumed, defined at the consumer, not pre-declared by the producer. S3Storage never declares that it satisfies Storage, and it doesn’t import the package that defines it. Go has no implements keyword, so the interface can sit next to the code that consumes it and list only the methods that consumer actually calls.
Do it the other way and the interface ends up living with the implementation: a storage package exporting both Storage and S3Storage, where the interface slowly grows to cover whatever any caller might want someday. Now every consumer depends on all of it, including the methods it never calls.
If your new feature has no dependencies to abstract over, no database, no external client, nothing you’d want to mock, it may need zero interfaces at all. A struct and methods is a complete answer.
3. Extract only when you actually need substitutability
Concretely, that’s one of:
- you want to unit test it with a mock or fake
- you expect more than one real implementation, S3 versus local disk
- a different layer needs to depend on it without knowing the concrete type, a service layer depending on
store.Storeinstead of*postgres.Store
If none of those apply yet, keep it concrete. Adding an interface “just in case” is the YAGNI mistake, and Go culture discourages pre-emptive interfaces for a specific reason: with no implements keyword there’s no refactor cost to adding one later, but there is a real cost to maintaining one you don’t need.
4. The constructor takes the interface type
Once Storage exists, the constructor accepts it instead of the concrete type. That’s the injection point.
type AvatarService struct { store Storage}
func NewAvatarService(store Storage) AvatarService { return AvatarService{store: store}}
func (a AvatarService) UploadAvatar(userID string, data []byte) error { return a.store.Save("avatars/"+userID, data)}AvatarService now knows about two methods rather than a specific storage backend.
5. main wires the concrete implementation in
This is where the interface actually pays off. main picks the concrete type and injects it as the interface into whatever depends on it.
func main() { store := S3Storage{Bucket: "my-app-uploads"} svc := NewAvatarService(store)
if err := svc.UploadAvatar("user-123", data); err != nil { log.Fatal(err) }}Switching to LocalStorage{BasePath: "./uploads"} for local development is one line here, and UploadAvatar never finds out. That’s the payoff, and it’s the last step rather than the first. I wrote more about the wiring itself in Dependency Injection in Go Is Just Passing In What You Need.
The sequence
| Step | What | Why |
|---|---|---|
| 1 | Struct and concrete methods | Get the real logic working |
| 2 | Look at what it depends on | Where interfaces get born |
| 3 | Narrow interface at the consumer | Avoids premature abstraction |
| 4 | Constructor takes the interface | The injection point |
| 5 | main picks the concrete type |
Composition root |
Takeaway
A useful gut check: if you can’t yet name a second real implementation, or a mock you’d write for a test, you probably don’t need the interface yet. Write the struct, ship it, and extract the interface the moment a second consumer or a test actually demands it.