Let’s talk about one of the most quietly powerful, “if you know, you know” principles in Go: accept interfaces, return structs. It sounds like a bumper sticker, but it’s the secret handshake that separates pleasant-to-use libraries from ones that feel like they’re actively fighting you.

The core idea is beautifully simple. Your functions and methods should be liberal in what they accept—ask for the smallest possible interface that gets the job done. But they should be conservative in what they return—give the user the concrete, most useful type you can. This maximizes flexibility for the caller and minimizes confusion about what they’re getting back.

Why This Principle is Your New Best Friend

Think about it from the caller’s perspective. If I’m using your function, I don’t want to rewire my entire program to pass in some specific YourAmazingService struct. I want to pass in my thing, the one that already has my database connection and logger configured, as long as it can DoTheThing(). By accepting an interface, you’re saying, “I don’t care about your entire implementation, I just need this one behavior.” This is the essence of loose coupling.

Returning concrete types, on the other hand, is about being helpful. If you return an interface, I, the caller, have no idea what the actual type is. I can’t assert to a more specific type to access other useful methods, and I can’t use any of the struct’s fields. You’ve handed me a black box. By returning a *ConcreteThing, you give me the full power of that type. I can choose to use it as-is, or I can assign it to an interface variable if I want to. The power is mine, not yours.

The Glorious Flexibility of Accepting Interfaces

Consider a simple function that writes a greeting. The bad, rigid way to write it would be to demand a specific concrete type, like a *os.File.

// Don't do this. It's inflexible and a pain to test.
func WriteGreetingBad(w *os.File) error {
    _, err := w.Write([]byte("Hello!\n"))
    return err
}

To test this, I’d have to actually create a file on disk. Yuck. Now, look at the beauty of accepting the io.Writer interface, which only requires a Write([]byte) (int, error) method.

// Do this. It accepts the world.
func WriteGreetingGood(w io.Writer) error {
    _, err := w.Write([]byte("Hello!\n"))
    return err
}

Suddenly, this function is infinitely more powerful. I can pass it:

  • An *os.File to write to a file.
  • A *bytes.Buffer to capture output in memory (perfect for testing!).
  • A *net.TCPConn to send a greeting over the network.
  • A *http.ResponseWriter to write an HTTP response.

My function didn’t get more complex; it became radically simpler and more versatile by relying on a small, standard interface.

The Pitfall of Returning Interfaces

Returning an interface is usually a misstep. It often creates an unnecessary abstraction and locks you in. Imagine a constructor that returns an interface.

// A questionable constructor
func NewThing() MyInterface {
    return &privateThing{}
}

Now, what if privateThing has a helpful Reset() method? As the caller, I can’t see it or use it because all I get is MyInterface. I’m stuck. You, the library author, have also painted yourself into a corner. You can never change the returned type without breaking the API, even if you want to return a privateThingV2 that also satisfies MyInterface.

Instead, return the concrete type. Always.

// A brilliant constructor
func NewThing() *Thing {
    return &Thing{}
}

// And later, I can use it directly or as an interface...
var thingObj *Thing = NewThing()
var writer io.Writer = NewThing() // if *Thing has a Write method

The caller gets all the functionality of *Thing and can choose how to use it. You retain the freedom to evolve the *Thing type.

The One Major Exception: Mocking

Alright, time for the obligatory nod to testing. Yes, sometimes you do need to return an interface: when you are explicitly returning a mock object from a helper function in your tests.

// In a test file, this is acceptable
func createMockThing() MyInterface {
    return &mockThing{}
}

This is fine because the purpose of the mockThing is to be nothing more than an implementation of MyInterface. Its entire existence is to be a stand-in. In your production code, however, stick to returning the real, concrete struct.

This pattern isn’t just dogma; it’s the accumulated wisdom of the Go community writing thousands of libraries. It makes your code a polite guest in someone else’s program instead of a demanding diva. Accept little, give much. It’s that simple.