Precision is the missing virtue in Go interfaces | alexriosTwo signatures. Same job. Read them carefully.
func SendWelcomeEmail(u User) error<br>func SendWelcomeEmail(email, name string) error<br>The first one accepts twenty fields to use two of them. The second one accepts exactly what it needs.
The difference between them isn’t style. It isn’t elegance. It isn’t even cleanness, whatever that means in 2026. It’s precision . The second signature is a promise that’s small enough to keep. The first one is a promise you’ll find yourself rewriting every time User shifts shape.
I’ve come to believe that precision in signatures is the single most undervalued virtue in Go codebases , and most of the design pain I help teams untangle comes from its absence. Not architecture. Not patterns. Not testing strategy. Just signatures that ask for more than they need.
What I mean by precision
A precise signature accepts the smallest input that lets the function do its job, and returns the smallest output that satisfies its callers. Nothing more.
type User struct {<br>ID string<br>Email string<br>Name string<br>PasswordHash string<br>CreatedAt time.Time<br>LastLoginAt time.Time<br>Roles []string<br>PreferredLang string<br>Timezone string<br>// ... and twelve more
func SendWelcomeEmail(u User) error {<br>return mailer.Send(u.Email, welcomeBody(u.Name))<br>SendWelcomeEmail uses two strings. It accepts twenty fields. That’s imprecision encoded in the type system . The signature is making promises the function doesn’t keep and demanding inputs the function doesn’t use.
The cost lands later, in three places I’ve watched it surface again and again:
Every test has to fabricate a User. Even the test for the welcome email behavior, where eighteen of those fields are irrelevant.
Every change to User invites the question “does this break SendWelcomeEmail?” The answer is almost always no, but you have to look, every time.
Every caller that doesn’t already hold a full User either fabricates one or routes around the function. The function is unreusable for half the cases that could have used it.
None of these are dramatic. They’re a small tax, levied every time the codebase changes, forever .
Precision is cheap in Go
Here’s what makes this frustrating: the precise version takes less work, not more.
func SendWelcomeEmail(email, name string) error {<br>return mailer.Send(email, welcomeBody(name))<br>Two strings. Zero opinions about the rest of User. Tests shrink. The function works for any caller with a name and an email, not only ones that happen to have a full User. The body of the function is unchanged. The signature is the only thing that moved.
This is the move I find myself recommending more than any other in code review, because it’s the one with the highest ratio of impact to effort. No new abstractions. No new patterns. No new tests. Just a smaller signature that’s truer to what the function actually does .
Where Go interfaces help, and where they hurt
The Go proverb is “accept interfaces, return structs.” Most developers I work with read it as a license to define interfaces. It should be read as a license to define small ones .
type Mailable interface {<br>Email() string<br>Name() string
func SendWelcomeEmail(m Mailable) error {<br>return mailer.Send(m.Email(), welcomeBody(m.Name()))<br>This interface is precise. It declares exactly the two capabilities the function uses. Any type with those two methods can be passed. The function knows nothing else about its caller.
This is fine. It’s also overkill for two strings, where the value version is simpler and equally precise. The interface earns its keep when there are multiple producers (a User from the DB, a Guest from a session, a Lead from a CRM), when the set of methods is small and stable, and when the function is reused widely enough that the indirection pays for itself.
The failure mode I see far more often is the fat interface :
type UserService interface {<br>Create(...) error<br>Update(...) error<br>Delete(...) error<br>Get(...) (User, error)<br>List(...) ([]User, error)<br>Authenticate(...) error<br>ResetPassword(...) error<br>// ... sixteen more methods<br>Someone wrote that because the concrete userService had twenty three methods. It’s an interface, technically. It’s not precise . Any function that accepts a UserService is implicitly claiming to need all twenty three capabilities, when it almost certainly needs two or three.
If your interface has more than four or five methods, ask which callers use which. You’ll almost always find the methods cluster into two or three subsets used by different parts of the system. Each subset is its own interface, declared next to the consumer that needs it.
That’s the move that’s actually idiomatic in Go. Tiny interfaces, defined where they’re consumed, accepting only what the consumer needs. Precision at every boundary, not just at the package boundary .
Precision compounds
This is the argument I find lands hardest in code review, because it’s not obvious from any one signature.
Imprecise...