Exploring Go's type system as an OO programmer
I’ve been learning Go over the pandemic as a way to learn new things. One of the things that I’ve had the hardest time with has been Go’s type system.
As a traditionally object-oriented programmer, we talk a lot about type hierarchies: all things come from some root object
class that the Compiler lies exists and eventually we get down into the brass tacks of real WidgetGrabber
s and IGrabbableWidget
s.
Coming from that background, the type system in Go feels awkard. However, if you stop thinking of it in the terms of an Object Oriented system and instead rewind time to the programming language equivalent of the Cambiran Explosion, Go isn’t too much unlike its other siblings: Ada, Oberon, Modula-2, etc.
In the end, though, Go’s type system is weird.
A while ago, I stopped using the term “Duck Typed” to describe Python. It’s not accurate! Duck has a distinct flavor that is unlike other things: gamey and with a hint of something you can only but qualify as “Definitely Ducklike”. There’s a few things that quack like a duck (geese, in particular) and the whole analogy falls apart when you know how unique ducks are. Heck, not even all ducks quack the same.
Chicken on the other hand is quite possibly the most non-descript fits-anywhere flavor there is. In the land of Python, everything can be reduced down to “Does it taste like a dict
?” Because Python’s focus in metaprogramming encourages you to make things up at runtime, the Python type system is really flexible.
There’s libraries that take advantage of this, too: boto3
for instance is extremely extensible and flexible, made possible in part by the extensibility of Python’s type system. Botocore and the derived libraries make heavy use of __getattr__
and such to whisp new types and functions into existence.
This works in an OO system: You can declare these hierarchies of interfaces and implementations. The compiler does a lot of the leg work to find the correct thing to use.
But Go isn’t Object Oriented. It looks like it, but it’s only OO in the way that C with some pointer references is “object oriented”.
Much like in C, Go has type definitions. Where C would say typedef int[] foo
, Go says type foo []int
. C and go both say that struct
s are a type. C says void*
, Go says interface{}
. Wait, hold on, interface
?
Go’s interface
system is both slightly familiar and wholly foreign to the average OO programmer and C programmer alike. It’s a promise to the compiler, but a one-sided promise.
You see, in an Object Oriented language like Java or C#, you’d declare an interface that you want to create;
public interface IThingable { /* ... */ }
say somewhere that you want something to take an IThingable
:
public void frob(IThingable thing) { // ...
Then later on declare that a thing implemements IThingable
:
public class Thing implements IThingable { /* ... * / }
The Java type system demands that you do this in order to maintain order from chaos, so it would seem. Java wants to make sure that the JIT has the ability to call all the things that our function is calling, and the interface makes that guarantee.
If I take and implement even the most simple of situations:
public interface IEmptyInterface { /* ... Empty ... */ }
/* ... */
public boolean DoSomething(IEmptyInterface empty) { /* ... */ }
To call this.DoSomthing("banana")
in Java would be heresy, the compiler would laugh, tell me to reassess my types, and fail the compilation.
Go? Go has no qualms with such a thing:
type Empty interface {}
func DoSomething(empty Empty) {
// ... Empty...
}
I can put literally anything into that. The following are all valid calls to DoSomething
:
DoSomething(nil)
DoSomething("bananas")
DoSomething([]int({1,2,3,4}))
This is because, like Python’s type system, Go’s types are composed, more than they’re constructed. Here’s a real example from some work on Restic that I’ve been doing:
// KDFParams are a mapping of string to interface, which will be JSON marshalled.
type KDFParams map[string]interface{}
//KDFImpl represents the most basic interface for a KDF.
type KDFImpl interface {
// construct takes a set of parameters and create a KDFEngine.
Construct(KDFParams) (KDFImpl, error)
// Derive a key from the given parameters, password, salt, and length
Derive(string, []byte, int) ([]byte, error)
// Calibrate creates a set of KDF parameters that cause a single
// round to take as close to but no more than the given time
Calibrate(time.Duration) (KDFParams, error)
// Validate a set of parameters, returning `nil` if ok, error otherwise.
Validate(KDFParams) (bool, error)
}
A lot of Go’s type system is on display here. I’ve given a convenient name to a map[string]interface{}
(what Python would call a Dictionary, and what you could feasibly call a Dict<string, object>
in C#) and then described an interface KDFImpl
that uses that. When implementing that interface, we don’t have to say we are, we just have to implement the actual functions:
type XorKDF struct {}
func (XorKDF _) Construct(KDFParams params) (KDFImpl, error) {
return XorKDF{}, nil
}
func (XorKDF xkd) Derive(string password, []byte salt, int len) ([]byte,error) {
// do something
return []byte({}), nil
}
func (XorKDF _) Calibrate(time.Duration _) (KDFParams, error) {
// ... Do something
return KDFParams{},nil
}
func (XorKDF _) Validate(KDFParams params) (bool, error) {
return true, nil
}
Here, we see the crux of Go’s interface type system: Interfaces are a declaration to the compiler that whatever goes in somewhere needs to have a particular shape, similar to how NEMA 5-20R receptacles are capable of handling either the 20 or 15A plugs:
As long as Go’s compiler can verify that the type you’re putting in can make the required calls, it doesn’t care what you put in.
- Author:indrora
- Permalink: https://zaibatsutel.net/posts/2021/go-oo-types/
- License: This text licensed under Creative Commons 4.0 BY-SA