
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.
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.
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.
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
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.
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.