Grokking Advanced Go
Vote

0% completed

​

Small Interfaces and Composition

Implicit Satisfaction

Single-Method Interfaces

Building Larger Interfaces

The Cost of More Methods

Checking Satisfaction at Compile Time

Key Takeaways

A well-known Go proverb, from Rob Pike, states: "The bigger the interface, the weaker the abstraction."

An abstraction here means a description of what code needs, with the details of any particular type removed. A large interface describes many details, so few types can satisfy it, and a parameter of that type does not tell the reader which of those methods the function calls. The interface io.Writer has one method, and implementing net.Conn from the net package takes eight. A search of the Go 1.27 standard library source, leaving out tests and the cmd directory, finds more than 100 methods with the signature of Write.

What makes one-method interfaces so widely useful, how are larger ones built from them, and how can code confirm at compile time that a type satisfies an interface?

Implicit Satisfaction

A Go type satisfies an interface when it has all of the interface's methods with the right signatures. No declaration links the two, and this is called implicit satisfaction. The package that defines the type does not mention the interface, and it does not need to import the package that declares it.

This rule makes small interfaces practical. The type strings.Builder in the standard library has a Write method, and nothing in its declaration mentions io.Writer. Any type written later, in any package, satisfies io.Writer in the same way as soon as it has the method. The method set comparison happens at compile time wherever the program converts a value to the interface.

Single-Method Interfaces

The standard library defines many interfaces with exactly one method. Each one describes a single capability that many unrelated types can offer.

InterfaceMethodSome types that satisfy it
io.ReaderRead(p []byte) (n int, err error)*os.File, *strings.Reader, *bytes.Buffer
io.WriterWrite(p []byte) (n int, err error)*os.File, *strings.Builder, *bytes.Buffer
fmt.StringerString() stringtime.Duration, *strings.Builder
errorError() string*fs.PathError, *strconv.NumError

A function that needs only one capability should declare a parameter that requires only that capability. The function below exports orders as lines of comma-separated text. It needs to write bytes and nothing else, so its first parameter is an io.Writer.

Go
Go

. . . .

Output:

1001,Ada,2500 1002,Lin,1250 builder holds 28 bytes buffer holds 28 bytes

The function ExportCSV does not change for any of the three destinations. The builder, the buffer, and standard output all receive the same calls. Writing to a file or to an HTTP response would need no change either, because both offer the same method. The pointers &builder and &buffer are the values that satisfy the interface, because Write has a pointer receiver on both types.

A one-method interface also lets you add new destinations. The program below declares a small type of its own, which counts bytes instead of storing them. It then uses io.MultiWriter, which returns one writer that duplicates each write to several others. The export function again needs no change.

Go
Go

. . . .

Output:

<nil> stored 28 counted 28 exporting order 1001: disk full

The last line shows a further benefit. To exercise the error path of ExportCSV, the program needs a writer that fails, and that takes only a few lines of code. With a parameter of type *os.File, the same check would need a real file that fails to write.

Building Larger Interfaces

An interface can list other interfaces among its methods, which is called embedding. The resulting method set is the union of all of them. The io package builds its larger interfaces in this way.

type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer }

Any value that satisfies io.ReadWriter also satisfies io.Reader and io.Writer, so it can be passed to a function that asks for either one. A function that receives an io.ReadWriter can read and write. A function that receives an io.Reader calls only Read through that parameter, even if the value it receives offers much more. The parameter type therefore states what the function expects to use.

Two embedded interfaces may contain the same method when the signatures are identical, and the method then appears once. If the signatures differ, the compiler rejects the declaration.

Interfaces built from small interfaces
Interfaces built from small interfaces

The Cost of More Methods

Three methods are enough to show the cost. The interface sort.Interface requires Len, Less, and Swap, because its algorithm needs all three. To sort orders by total with sort.Sort, a program must declare a new type and write all three methods for it, and it must do so again for each further sort order. Only Less differs between those types. The function slices.SortFunc asks instead for a single comparison function, and the documentation of sort.Sort itself notes that slices.SortFunc is more ergonomic and runs faster in many situations.

Go
Go

. . . .

Output:

[{1002 1250} {1001 2500} {1003 4000}] [{1002 1250} {1001 2500} {1003 4000}]

Both calls give the same result. The version with an interface needs a type and three methods, and the other needs one function. That difference appears with only three methods. With an interface of fifteen methods, every implementer must write all fifteen, including a small type written only to check one function.

Small interfaceLarge interface
Types that can satisfy itMany, often unrelatedFew, often only one
Work to write a new implementationA few linesOne method body per method
What a parameter of this type tells the readerExactly what the function usesVery little

Checking Satisfaction at Compile Time

Implicit satisfaction has one drawback. If a type is meant to satisfy an interface but its method has the wrong signature, the compiler reports nothing until some code performs the conversion. If that conversion is in another package, or if the only use goes through any, building the type's own package reports nothing, and the mistake appears only when that other package is compiled or the program runs.

A single declaration at package level makes the compiler check the intention in the type's own package.

Go
Go

. . . .

Output:

1001,2500

The expression (*CSVExporter)(nil) converts nil to the type *CSVExporter. Assigning it to a variable named _ keeps nothing, so the line uses no memory at run time, and the compiler still has to check the conversion. This program never converts the type anywhere else, so the check is the only line that tests it. If the method loses its error result, the build fails, and the first error gives the reason.

./main.go:21:18: cannot use (*CSVExporter)(nil) (value of type *CSVExporter) as Exporter value in variable declaration: *CSVExporter does not implement Exporter (wrong type for method Export) have Export(io.Writer, []Order) want Export(io.Writer, []Order) error

Use a value of the type, such as var _ Exporter = CSVExporter{}, when the methods have value receivers. This declaration is worth writing when a type exists mainly to satisfy an interface and no code nearby performs the conversion. It adds nothing when the same package already converts the type to the interface.

Key Takeaways

  • A type satisfies an interface by having its methods, and no declaration links the two.
  • A one-method interface such as io.Writer describes one capability, so many unrelated types can satisfy it.
  • A parameter should use the smallest interface that covers what the function calls.
  • Larger interfaces are built by embedding small ones, as io.ReadWriter is built from io.Reader and io.Writer.
  • Each extra method raises the cost of every implementation, as sort.Interface shows with only three methods.
  • var _ Exporter = (*CSVExporter)(nil) makes the compiler check satisfaction, at no cost at run time.

A small interface states one thing that a function needs, and that is why so many types can meet it. The export function in this lesson works with a builder, a buffer, standard output, and a type written a moment ago, because its parameter requires only a Write method. Larger interfaces remain available when a function truly needs more, and they should be assembled from the small ones.

Reading Progress

0%


Vote for new content

On This Page

Implicit Satisfaction

Single-Method Interfaces

Building Larger Interfaces

The Cost of More Methods

Checking Satisfaction at Compile Time

Key Takeaways