A common mistake is to define an interface before any caller needs one. An interface describes behavior, and Go types satisfy it implicitly. That makes interfaces useful for interchangeable implementations, but an unnecessary interface adds indirection and makes code harder to follow. Start with the concrete problem; introduce an abstraction when it solves one. section 2.5.1 section 2.5.3
func copySourceToDest(source io.Reader, dest io.Writer) error {
// ...
}
A copy function can accept io.Reader and io.Writer instead of two *os.File values. It then works with files or other implementations of the same read and write behaviors. section 2.5.1
func TestCopySourceToDest(t *testing.T) {
const input = "foo"
source := strings.NewReader(input) #1
dest := bytes.NewBuffer(make([]byte, 0)) #2
err := copySourceToDest(source, dest) #3
if err != nil {
t.FailNow()
}
got := dest.String()
if got != input {
t.Errorf("expected: %s, got: %s", input, got)
}
}
The same signature also makes a test possible with a strings.Reader and a bytes.Buffer, without managing files. section 2.5.1
Abstract the dependency a caller actually uses
A service tied directly to mysql.Store must use that concrete store when testing its storage call. If the service only calls StoreCustomer, it can instead depend on a small customerStorer interface. This decouples the service from that implementation and permits a replacement for testing. section 2.5.2
type customerStorer interface { #1
StoreCustomer(Customer) error
}
type CustomerService struct {
storer customerStorer #2
}
func (cs CustomerService) CreateNewCustomer(id string) error {
customer := Customer{id: id}
return cs.storer.StoreCustomer(customer)
}
The service declares only the StoreCustomer method it needs; its field uses that interface rather than mysql.Store. section 2.5.2
Choose the boundary deliberately. A function can often accept an interface while returning a concrete type, leaving callers free to choose their own abstractions. Returning an interface can force every caller into one abstraction, though established interfaces such as io.Reader can justify exceptions. Generics can also remove boilerplate, but premature type parameters create the same kind of unnecessary complexity. section 2.7 section 2.17














