All the types we’ve looked at so far have been concrete types.
A concrete
type specifies the exact representation of its values and exposes the
intrinsic operations of that representation, such as arithmetic for
numbers, or indexing, append
, and range
for slices. A concrete
type may also provide additional behaviors through its methods.
When you have a value of a concrete type, you know exactly what it
is and what you can do with it.
There is another kind of type in Go called an interface type. An interface is an abstract type. It doesn’t expose the representation or internal structure of its values, or the set of basic operations they support; it reveals only some of their methods. When you have a value of an interface type, you know nothing about what it is; you know only what it can do, or more precisely, what behaviors are provided by its methods.
Throughout the book, we’ve been using two similar functions for string
formatting: fmt.Printf
, which writes the result to the standard
output (a file), and fmt.Sprintf
, which returns the result as a
string
.
It would be unfortunate if the hard part, formatting the result, had
to be duplicated because of these superficial differences in how
the result is used. Thanks to interfaces, it does not.
Both of these functions are, in effect, wrappers around a third function,
fmt.Fprintf
, that is agnostic about what happens to the result it
computes:
package fmt func Fprintf(w io.Writer, format string, args ...interface{}) (int, error) func Printf(format string, args ...interface{}) (int, error) { return Fprintf(os.Stdout, format, args...) } func Sprintf(format string, args ...interface{}) string { var buf bytes.Buffer Fprintf(&buf, format, args...) return buf.String() }
The F
prefix of Fprintf
stands for file and
indicates that the formatted output should be written to the file
provided as the first argument.
In the Printf
case, the argument, os.Stdout
, is an
*os.File
.
In the Sprintf
case, however, the argument is not a file, though it
superficially resembles one: &buf
is a pointer to a memory
buffer to which bytes can be written.
The first parameter of Fprintf
is not a file either.
It’s an io.Writer
, which is an interface type with the
following declaration:
package io // Writer is the interface that wraps the basic Write method. type Writer interface { // Write writes len(p) bytes from p to the underlying data stream. // It returns the number of bytes written from p (0 <= n <= len(p)) // and any error encountered that caused the write to stop early. // Write must return a non-nil error if it returns n < len(p). // Write must not modify the slice data, even temporarily. // // Implementations must not retain p. Write(p []byte) (n int, err error) }
The io.Writer
interface defines the contract between
Fprintf
and its callers.
On the one hand, the contract requires that the caller provide a value
of a concrete type like *os.File
or *bytes.Buffer
that
has a method called Write
with the appropriate signature
and behavior.
On the other hand, the contract guarantees that Fprintf
will do
its job given any value that satisfies the io.Writer
interface.
Fprintf
may not assume that it is writing to a file or to
memory, only that it can call Write
.
Because fmt.Fprintf
assumes nothing about the representation of the
value and relies only on the behaviors guaranteed by the
io.Writer
contract, we can safely pass a value of any concrete type that
satisfies io.Writer
as the first argument to fmt.Fprintf
.
This freedom to substitute one type for another that satisfies the
same interface is called substitutability, and is a hallmark of
object-oriented programming.
Let’s test this out using a new type.
The Write
method of the *ByteCounter
type below merely
counts the bytes written to it before discarding them.
(The conversion is required to make the types of len(p)
and
*c
match in the +=
assignment statement.)
type ByteCounter int func (c *ByteCounter) Write(p []byte) (int, error) { *c += ByteCounter(len(p)) // convert int to ByteCounter return len(p), nil }
Since *ByteCounter
satisfies the io.Writer
contract, we
can pass it to Fprintf
, which does its string formatting oblivious to
this change; the ByteCounter
correctly accumulates the
length of the result.
var c ByteCounter c.Write([]byte("hello")) fmt.Println(c) // "5", = len("hello") c = 0 // reset the counter var name = "Dolly" fmt.Fprintf(&c, "hello, %s", name) fmt.Println(c) // "12", = len("hello, Dolly")
Besides io.Writer
, there is another interface of great importance to the fmt
package.
Fprintf
and Fprintln
provide a way for types to control
how their values are printed.
In Section 2.5, we defined a String
method
for the Celsius
type so that temperatures would print as
"100°C"
, and in Section 6.5 we equipped *IntSet
with a String
method so that sets would be rendered using
traditional set notation like "{1 2 3}"
.
Declaring a String
method makes a type satisfy one of the most
widely used interfaces of all, fmt.Stringer
:
package fmt // The String method is used to print values passed // as an operand to any format that accepts a string // or to an unformatted printer such as Print. type Stringer interface { String() string }
We’ll explain how the fmt
package discovers which values satisfy this
interface in Section 7.10.
Exercise 7.1:
Using the ideas from ByteCounter
, implement counters for
words and for lines.
You will find bufio.ScanWords
useful.
Exercise 7.2:
Write a function CountingWriter
with the signature below that,
given an io.Writer
, returns a new Writer
that wraps
the original, and a pointer to an int64
variable that at
any moment contains the number of bytes written to the new Writer
.
func CountingWriter(w io.Writer) (io.Writer, *int64)
Exercise 7.3:
Write a String
method for the *tree
type in
gopl.io/ch4/treesort
(§4.4) that reveals the
sequence of values in the tree.
3.141.35.60