liveBook · Manning

Contract

As explained in 100 Go Mistakes and How to Avoid Them

An application programming interface is a defined set of operations and conventions through which software components interact.

An API is the set of operations and conventions callers use to interact with a component. In Go, convenient APIs make those operations expressive without hiding useful type information. A common mistake is to design an interface for every implementation before any caller needs one; the extra indirection can make code harder to follow. section 1.3.5 section 2.5.3

Use an interface when a caller needs common behavior, decoupling, or a restricted set of operations. Go types satisfy interfaces implicitly, so a caller can define only the methods it needs. For example, a function accepting `io.Reader` and `io.Writer` can copy between files or other implementations, and can be tested with a string reader and byte buffer. section 2.5.1 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 depends on the `StoreCustomer` behavior rather than a particular store. This abstraction is useful when the service needs to accept another implementation; it need not be created merely because a concrete store exists. section 2.5.2 section 2.5.3

Check an API from the caller’s side:

  • Do not force every client through a broad interface defined beside the implementation. A client that only retrieves customers can define a one-method interface where it is used. section 2.6
  • Usually return a concrete implementation and accept an interface where useful. Returning an interface can force one abstraction on every caller and create awkward package dependencies; `error` is a notable exception. section 2.7
  • Avoid `any` unless arbitrary types are genuinely required. Use functional options when an API needs convenient options rather than choosing a creation pattern by habit. section 2.17

A signature can affect allocations

`Read(p []byte) (n int, err error)` lets the caller supply a buffer. An alternative, `Read(n int) (p []byte, err error)`, would cause the returned slice to escape to the heap. The caller-supplied form does not guarantee stack allocation, but it avoids making that escape a consequence of the signature. When allocations matter, inspect the compiler’s decision with `-gcflags`. section 12.6.1

Return values deserve the same care as parameters: returning an interface that contains a nil pointer does not give the caller a nil interface value. Return an explicit nil value when that is the intended result. section 6.7

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Works through interface placement and return-type tradeoffs, then traces one signature’s allocation effect; it does not prescribe a universal interface rule.
Examples
Uses customer storage, `io.Reader` and `io.Writer`, and contrasting `Read` signatures to expose caller and allocation consequences.
What it assumes
Assumes familiarity with Go functions, packages, structs, and interfaces, while explaining implicit interface satisfaction through I/O examples.
What you can do after
Build it from scratch with interfaces shaped by callers, concrete return types where appropriate, and signatures checked for allocation effects.
How it connects
Connects API design to Go interfaces, package dependencies, functional options, generics, and escape analysis.

Also explained in

Compare how 44 books explain contract →

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.