How to learn and work with Go's types?

I’m going to add to Holloway’s answer with some info that I hope will be useful:

There are two “classifications”* of types in Go: concrete and interface types.

Concrete types are:

  • All of Go’s built in types like bool, int, string, etc.
  • All structs
  • All arrays
  • All pointers
  • All slices
  • All maps
  • All functions

That is, concrete types are everything. Everything in Go that has a type is of a concrete type. If you want to convert from one concrete type to another, you have to use a conversion.

Interface types are descriptions of required methods that must exist on a concrete type in order to implement the interface. For example:

io.Reader is an interface type and therefore consists of just a method set:

type Reader interface {
        Read(b []byte) (n int, err error)
}

That is to say that any concrete type that has a Read method with the same signature (one []byte slice parameter and returns (int, error)), then that concrete type implements io.Reader. io.ReadCloser is also an interface type:

type ReadCloser interface {
        Read(b []byte) (n int, err error)
        Close() error
}

Because the io.ReadCloser interface defines the same Read method as io.Reader, then if your type currently implements io.Reader and you add a Close method that returns an error now your type also implements io.ReadCloser.

Putting it together a bit more:

The Body field of the http.Response type is of type io.ReadCloser. We know now that io.ReadCloser is just a method set. You never actually have a value of type io.ReadCloser, the value has to have a real, concrete type, and that type must have Read and Close methods just like the io.ReadCloser interface requires.

If you have a value (of some concrete type, which we don’t actually care about) that implements io.ReadCloser and you want to use it as an io.Reader, no conversion is necessary because you’re going from an interface type that contains a Read method to another interface type that contains the same Read method.

Consider the following example:

package main

import (
	"fmt"
	"io/ioutil"
)

type myType struct{}

func (myType) Read(a []byte) (b int, err error) {
	return 0, fmt.Errorf("TODO!")
}

type myReader interface {
	Read(b []byte) (n int, err error)
}

func main() {
	var x myReader = myType{}
	bs, err := ioutil.ReadAll(x)
	fmt.Println("bs:", bs, "err:", err)
}

I made my own myReader type for no reason here other than to demonstrate that interface types just define method sets and if the compiler knows at compile time that my x variable is of some type that implements the myReader method set, then the compiler knows that ioutil.ReadAll requires a value that implements the io.Reader method set which is the same set as myReader, so it just works.

An *os.File doesn’t have a Body field because an *os.File itself already has the Read and Close methods on it.


* Classification is a word I chose here but isn’t an official term for this divide, so I suspect you won’t see it called this way anywhere else.

4 Likes