Farsight TXT Record

Fun With Go Method Routing

Written by: 
Published on: 
Apr 12, 2016
On This Page
Share:

Introduction

Many of us here at Farsight Security are fans of the (relatively) newGo programming language, and are using it in severalprojects. In addition to its powerful standard library, Go is notable for aclean C-like syntax, integrated support for concurrency, anda simple yet powerful type system. This article focuses on a subtletyof 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 fieldis 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 stringrepresentation 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 relativeto 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 theuser 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, althoughdefined in

FileProcessor

, calls the

capitalizer

implementation of

Process

.

The

Process

method here is said to be “virtual”. All python methodsare virtual, it is the default behavior of methods in Java, and canbe requested with the “virtual” keyword in C++. In contrast, Go has no virtualmethods. 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 tothe

FileProcessor

abstraction. Instead of making it the base type,

FileProcessor

becomes the outer type, and users fill in behaviorwith 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 doesnot allow multiple FileProcessor instances with different processing:all

FileProcessor

s are

capitalizer

s in this example.

To overcome this last hurdle, we use Go’s

interface

types. An

interface

is merely a collection of methods, and a variableor field with an interface type can carry any value whose underlying typesupports 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 aconstructor for a capitalizing

FileProcessor

, and willmirror the complexity of the equivalent inheritance hierarchy.

Conclusion

While abstract base classes with virtual methods are commonin object-oriented libraries, the Go programming languagedoes not support (and is arguably actively hostile to) thisdesign approach. Even with this non-support, it is possibleto realize much of the flexibility of this approach by usingobject composition, which Go’s embedding mechanism makesalmost as convenient as typical object-oriented class inheritance.

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