liveBook · Manning

Abstraction

As explained in 100 Go Mistakes and How to Avoid Them

A software design technique that represents essential behavior or structure while hiding implementation details.

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

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Explains implicit interface satisfaction, works through I/O and storage examples, and diagnoses premature interfaces without developing interface internals.
Examples
Uses a reader-to-writer copy test, a sortable collection, and a customer storage dependency.
What it assumes
Assumes familiarity with Go functions and types while explaining interface behavior and implicit satisfaction.
What you can do after
Build it from scratch by defining a small Go interface when a caller needs interchangeable behavior, rather than predicting future needs.
How it connects
Connects interfaces to code reuse, dependency decoupling, unit testing, generics, and API return types.

Also explained in

Compare how 15 books explain abstraction →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.