Fun With Go Method Routing

← Blog Home

RSS

By

Introduction

Many of us here at Farsight Security are fans of the (relatively) new Go programming language, and are using it in several projects. In addition to its powerful standard library, Go is notable for a clean C-like syntax, integrated support for concurrency, and a simple yet powerful type system. This article focuses on a subtlety of the type system.

Embedding, Inheritance

All Go types, struct or not, can have methods. Struct types, however, can “embed” other types. Embedding is similar to having a field with the embedded type, but this field is unnamed and, most significantly, all methods of the embedded type are “promoted” to the embedding struct. For example, in the following:

type T struct { A, B int }
func (t *T) String() string {
	return fmt.Sprintf("T: %d, %d", t.A, t.B)
}

type T2 struct { T }

type T2 will also have a String() method, returning the string representation of the embedded type T.

Although the promotion of methods (and, since T is a struct, fields) to T2 may superfically resemble inheritance (“is a”), it is composition (“has a”) under the hood. This affects method routing significantly relative to other object-oriented languages.

Going Virtual

A common object-oriented library structure is the “abstract base class”, where a library provides a type with some high level methods defined on it, defines some methods with no (or stub) implementations, and relies on the user to provide implementations of these methods in derived classes. A python example would be:

class FileProcessor:
    def __init__(self, in, out):
        self.in = in
        self.out = out

    # default process is to do nothing
    def Process(self, line):
        return line

    def run(self):
        while True:    
            line = self.in.readline()
	    if line == "":
                return
            self.out.write(self.Process(line))

class capitalizer(FileProcessor):
    def Process(self, line):
        return line.upper()

An instance of capitalizer will have a run method which, although defined in FileProcessor, calls the capitalizer implementation of Process.

The Process method here is said to be “virtual”. All python methods are virtual, it is the default behavior of methods in Java, and can be requested with the “virtual” keyword in C++. In contrast, Go has no virtual methods. Consider the following code:

type FileProcessor struct {
	in *bufio.Reader
	out io.Writer
}

func (f *FileProcessor) Process(line string) string { return line }

func (f *FileProcessor) Run() {
	for {
		line, err := f.in.ReadString('\n')
		if err != nil {
			break
		}
		io.WriteString(f.out, f.Process(line))
	}
}

type capitalizer struct {
	*FileProcessor
}

func (c *capitalizer) Process(line string) string {
	return strings.ToUpper(line)
}

The capitalizer implementation of Process will never be called. The capitalizer Run method calls the FileProcessor Run method, which will always call the FileProcessor implementation of Process, because its method receiver *f is of type *FileProcessor.

Inside Out

The above case would work, sort of, if we inverted our approach to the FileProcessor abstraction. Instead of making it the base type, FileProcessor becomes the outer type, and users fill in behavior with composition. So, the Go version becomes:

type capitalizer struct {}

func (c *capitalizer) Process(line string) string {
	return strings.ToUpper(line)
}

type FileProcessor struct {
	in  *bufio.Reader
	out io.Writer
	*capitalizer
}

This, along with removing the FileProcessor implementation of Process, will do what we wanted capitalizer to do. However, this does not allow multiple FileProcessor instances with different processing: all FileProcessors are capitalizers in this example.

To overcome this last hurdle, we use Go’s interface types. An interface is merely a collection of methods, and a variable or field with an interface type can carry any value whose underlying type supports those methods. In this case:

type StringProcessor interface {
	Process(string) string
}

type FileProcessor struct {
	in  *bufio.Reader
	out io.Writer
	StringProcessor
}

the StringProcessor element can take a value of any type which has a method Process with a single string argument, returning a single string.

The initialization of this new FileProcessor is slightly clunky:

	fp := &FileProcessor{in, out, &capitalizer{}}

but in practice, this complexity will be hidden in a constructor for a capitalizing FileProcessor, and will mirror the complexity of the equivalent inheritance hierarchy.

Conclusion

While abstract base classes with virtual methods are common in object-oriented libraries, the Go programming language does not support (and is arguably actively hostile to) this design approach. Even with this non-support, it is possible to realize much of the flexibility of this approach by using object composition, which Go’s embedding mechanism makes almost as convenient as typical object-oriented class inheritance.

Chris Mikkelson is a Senior Distributed Systems Engineer for Farsight Security, Inc.

← Blog Home

Protect against cybercriminal activity in real-time.

Request demo

Email: sales@farsightsecurity.com Phone: +1-650-489-7919