Go: Interfaces

Love them or hate them, the Go programming language has interfaces. The term interface in programming has overloaded semantics behind it, so the TLDR of interfaces are that they are an abstract type that describes a set of method signatures. In some languages, you’ll explicitly state whether or not a particular type needs to implement an interface as part of the declaration of the type; however, in Go, interfaces are satisfied implicitly. In other words, we don’t have to declare all the interfaces that a concrete type satisfies. The Go compiler will be able to resolve whether or not a particular type implement an interface. There are some neat and powerful concepts in Go using interfaces, so let’s start taking a look at what they’re all about!

Interface Declarations

Go allows you to define your own interfaces, which again is an abstract type. When interfaces are used as values, all that is required is the concrete type implement the methods signatures attached to the interface. This is the implicit satisfaction that occurs with Go, we don’t need to explicitly state that a concrete type implements a particular interface.

Let’s create a simple interface in go called FileReader, which will contain a single function ReadData([]map[string]string, error) and we will implement two concrete types for reading CSV (comma separated values) and TSV (tab separated values) files.

package main

import (
    "encoding/csv"
    "fmt"
    "os"
    "strings"
)

type FileReader interface {
    ReadData() ([]map[string]string, error)
}

We can create a CSV Reader concrete type that implements a method called ReadData in order to fulfill the new interface FileReader.

type CSVReader struct {
    FilePath string
}

func (c *CSVReader) ReadData() ([]map[string]string, error) {
    file, err := os.Open(c.FilePath)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    reader := csv.NewReader(file)
    records, err := reader.ReadAll()
    if err != nil {
        return nil, err
    }
    header := records[0]
    var data []map[string]string
    for _, row := range records[1:] {
        record := make(map[string]string)
        for i, value := range row {
            record[headers[i]] = value
        }
        data = append(data, record)
    }
    return data, nil
}

Then, we will create our TSV Reader concrete type that will also fulfill the FileReader interface.

type TSVReader struct {
    FilePath string
}

func (t *TSVReader) ReadData() ([]map[string]string, error) {
    file, err := os.Open(t.FilePath)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    reader := csv.NewReader(file)
    reader.Comma = '\t'
    records, err := reader.ReadAll()
    if err != nil {
        return nil, err
    }

    headers := records[0]
    var data []map[string]string
    for _, row := range records[1:] {
        record := make(map[string]string)
        for i, value := range row {
            record[headers[i]] = value
        }
        data = append(data, record)
    }
    return data, nil
}

With these two concrete types defined we can create a function which argument is the interface type of FileReader and call the interface’s defined function.

func ProcessFile(reader FileReader) {
    data, err := reader.ReadData()
    if err != nil {
        fmt.Printf("Error reading data: %v\n", err)
        return
    }

    for _, record := range data {
        fmt.Println(record)
    }
}

func main() {
    csvReader := CSVReader{FilePath: "data.csv"}
    tsvReader := TSVReader{FilePath: "data.tsv"}
    fmt.Println("Reading CSV File:")
    ProcessFile(csvReader)
    fmt.Println("\nReading TSV File:")
    ProcessFile(tsvReader)
}

Interface Examples: io.Reader and io.Writer

We’ve now see what it looks like to define our own interface, but let’s take a look at one of the most interesting and useful interfaces that the Go standard library has to offer the io.Reader and io.Writer. These interfaces are defined as follows:

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

type Writer interface {
	Write(p []byte) (n int, err error)
}

The function io.Copy takes both a Reader and a Writer as arguments. With these interfaces we can create a byte buffer that contains data that can be redirected to a particular kind of writer, in our examples we’ll use standard out; however, the writer could be anything that implements the Writer interface.

package main

import (
	"bytes"
	"io"
	"os"
)

func main() {
	data := []byte("this is a byte slice of data")
	reader := bytes.NewBuffer(data)
	if _, err := io.Copy(reader, os.Stdout); err != nil {
		panic(err)
	}
}

One again, a simple example of creating a new instances of a struct that implements the Reader interface and using the os.Stdout as a Writer. Let’s keep going, we can actually do the same with with a file as a Reader and output the contents of the file to os.Stdout.

package main

import (
    "io"
    "os"
)

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    _, err = io.Copy(os.Stdout, file)
    if err != nil {
        panic(err)
    }
}

In Go, a net.Conn interface contains the same signatures as the io.Reader and io.Writer, meaning that any concrete type that implements the net.Conn interface also implements both he io.Reader and io.Writer interfaces. Let’s create a simple example of this by creating a TCP server and client, we’ll use the io.ReadAll function on the server side which requires a io.Reader to be used and on client side we’ll use the implemented Write method that is attached to our concrete connection type that implements the io.Writer as another way to show how powerful these interfaces can be.

// server
package main

import (
    "fmt"
    "io"
    "net"
)

func main() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer listener.Close()
    fmt.Println("Server is listening on port 8080...")
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Error accepting connection:", err)
            continue
        }
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    fmt.Println("Client connected.")
    // Read data from client (implements io.Reader)
    data, err := io.ReadAll(conn)
    if err != nil {
        fmt.Println("Error reading data:", err)
        return
    }

    fmt.Printf("Received from client: %s\n", string(data))
}

// client
package main

import (
	"fmt"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	message := "Hello from client!"
	// Write to server (conn implements io.Writer)
	_, err = conn.Write([]byte(message))
	if err != nil {
		fmt.Println("Error writing to server:", err)
		return
	}
	fmt.Println("Message sent to server.")
}

I often find myself looking toward to Go standard library for influence on elegant and simple API design. Now, with that said, there are some rough edges of any language and standard library. For the most part, I find a lot of inspiration and influence in the standard library and the io.Reader and io.Writer interfaces are one of many great examples of how to compose power interfaces that allow for numerous concrete type implements.

Interface Point to a Nil Pointer is Non-Nil?

This topic is pretty interesting, let’s just talk about pointers in Go for a second. A pointer in Go is a variable that contains it’s own memory address, but the value of the variable is the memory address location that contains a variable of that particular type. When we define a variable as a pointer in Go, its default value that is provided is nil. In the below example, we should see the output value of <nil> indicating that indeed the default value was set to nil for a pointer variable.

package main

import "fmt"

func main() {
	var a *int
	fmt.Printf("%v\n", a)
}

If we edit the above code and give our variable another variable to point to, we should be able to print out the address of the variable we’re pointing to, the variables addresses, and the value.

package main

import "fmt"

func main() {
	var i int
	i = 256
	fmt.Printf("i address: %p - i value: %d\n", &i, i)
	var a *int
	a = &i
	fmt.Printf("a address: %p - a value: %p - a actual value: %d\n", &a, a, *a)
}

Alright, that’s about as much basic pointer stuff as we’re going to cover for the moment. Let’s talk about interfaces and pointers of types that implement particular interfaces, what would happen if we had a concrete type pointer variable of which the type implemented a particular interface but we never set the pointer to point to an actual instance of the type? In other words, what would happen if we had a nil pointer that implements an interface?

package main

import (
	"fmt"
	"strings"
)

type FieldPrinter interface {
	PrintFields() string
}

type MetaData struct {
	ID   int
	Name string
}

func (md *MetaData) PrintFields() string {
	sb := strings.Builder{}
	sb.WriteString(fmt.Sprint("ID: %d\n", md.ID))
	sb.WriteString(fmt.Sprint("Name: %d\n", md.Name))
	return sb.String()
}

func main() {
	var metaDataPtr *MetaData
	var fieldPrinter FieldPrinter
	if fieldPrinter == nil {
		fmt.Print("fieldPrinter is nil\n")
	}
	if metaDataPtr == nil {
		fmt.Print("metaDataPtr is nil\n")
	}
	fieldPrinter = metaDataPtr
	if fieldPrinter == nil {
		fmt.Print("fieldPrinter is nil\n")
	} else {
		fmt.Print("fieldPrinter is NOT nil?\n")
	}
}

Running the above code would show us that after we assign the interface variable to a pointer type that implements that interface the interface variable is no longer nil? Well, this brings us to another point in this article that is a bit odd, can you call a method on a nil pointer? Well, the answer is yes you can.

package main

import (
	"fmt"
)

type MetaData struct {
	ID   int
	Name string
}

func (md *MetaData) CheckNil() {
	if md == nil {
		fmt.Printf("metadata is nil\n")
	} else {
		fmt.Printf("metadata is not nil\n")
	}
}

func main() {
	var metaDataPtr *MetaData
	metaDataPtr.CheckNil()
}

When we think of methods in Go, we should really just be thinking about a function call that either contains a pointer or value, depending on the method receiver type, that is placed as the first argument to a function call. So the method func (md *MetaData) CheckNil() ... should really be look at like func CheckNil(md *MetaData) .... Bring it back to why an interface becomes non-nil when we assign a interface to a nil pointer that implements that particular interface, for drawing a basic idea we can say that an interface variables have a type component and a value component. If the type component is a pointer type and the value is nil, technically the type does implement the interface and since we just created an example that shows in some scenarios it is valid to call a method on a nil pointer, Go has to set the interface variable to non-nil to avoid a scenario where an interface implementation may not directly access the pointer variable.

Type Assertion

A type assertion is an operation that can be applied to an interface value that checks the dynamic type of its operand matches the asserted type. A type assertion to a concrete type extracts the concrete value from its operand. If the check fails, then the operation panics or returns an error depending on how to assertion is applied.

var w io.Writer
w = os.Stdout
f := w.(*os.File)       // success
c := w.(*bytes.Buffer)  // panics

In order to avoid a panic, we can test whether it is some particular type. If the type assertion appears in an assignment in which two results are expected, the operation does not panic on failure but instead returns a boolean value that indicates the success of the assertion.

var w io.Writer = os.Stdout
f, ok := w.(*os.File)       // success, ok value true
b, ok := w(*bytes.Buffer)   // failure, ok value failure

Type switches simplifies an if-else chain that does a series of type value equality checks.

switch x.(type) {
    case nil:       // ...
    case int, uint: // ...
    case bool:      // ...
    case string:    // ...
    default:        // ...
}

VTable (Virtual Method Table)

In OOP languages, VTables or virtual method tables are a component for enabling polymorphic behavior. A VTable is essentially a lookup table used to support dynamic dispatching of virtual functions, each class that has a virtual method gets is own VTable, each object of that class carries a pointer, let’s call it vptr, that points to the VTable of its class. The VTable contains pointers to the actual implementations of the virtual functions. When a method is called on an object, vptr is used to find the appropriate method address in the VTable, allowing the correct function to be executed at runtime.

This mechanism provides the functionality required for runtime polymorphism, enabling behaviors in derived classes to be invoked through base class pointers or references; however, it also has a performance cost due to the indirection involved in accessing function pointer and memory overhead from storing the VTable.

Cool story Nick, what does this have to do with anything that we were talking about previous?

Well, I’m glad that you asked, we’ll be talking about how interfaces work in Go and how similar it is to VTables.

The iface and itab

Go’s interfaces provide a way to achieve polymorphism, which approach will seem strikingly similar to VTables. Go has structures called iface and itab, these types allow dynamic dispatching similar to the VTable mechanism we discussed in the previous section.

The iface structure represents an instance of an interface that contains a pointer to the actual data (concrete type) , and a pointer to an itab, which helps resolve the appropriate method implementations. The itab structure helps link the interface methods to the methods implemented by the concrete type, this structure contains metadata about the type implementing the interface and pointers to the functions that implement the interface methods.

If we were to write our own conceptual implementation, it could look something like the following block of code:

package main

import (
    "fmt"
    "reflect"
)

type methodFunc func(interface{})

type itab struct {
    typeName string
    methods  map[string]methodFunc
}

type iface struct {
    tab  *itab
    data interface{}
}

type Dog struct{}

func (d Dog) Speak() {
    fmt.Println("Woof!")
}

func createItab(typeName string, methods map[string]methodFunc) *itab {
    return &itab{
        typeName: typeName,
        methods:  methods,
    }
}

func (i *iface) Call(methodName string) {
    if method, ok := i.tab.methods[methodName]; ok {
        method(i.data)
    } else {
        fmt.Printf("Method %s not found for type %s\n", methodName, i.tab.typeName)
    }
}

func main() {
    dogItab := createItab("Dog", map[string]methodFunc{
        "Speak": func(data interface{}) {
            // Type assertion to call the actual method
            if d, ok := data.(Dog); ok {
                d.Speak()
            }
        },
    })
    animalInterface := &iface{
        tab:  dogItab,
        data: Dog{},
    }
    animalInterface.Call("Speak")
}

Obviously, this is over simplified but I think paints a clear picture of what is trying to be achieved.

SIGSTOP

“I can only show you the door, you’re the one that has to walk through it.”

References