Golang Interface Flexibility
Coming from JavaScript into Golang, I am used to some of the most extreme flexibility you can find in programming. Consider this random snippet:
// yay! an array of anything I could possibly imagine!
let allTheThings = [{
"let's use a string",
12345,
"and numbers",
[1,2,3,4, "welp, and a nested array..."],
{ name: "andAnObject" },
function() { return "why not a func also?" },
() => "and an arrow func",
// etc
}]
The value of the above is questionable, it is hard to know what you can do with each of the items in the array. At best, you have an implicit contract somewhere expecting a certain order of components.
In Golang, you have strong typing. This is valuable, but initially feels
limiting. Your first list (slice
in Golang is the most similar to an
array
in JavaScript) may look something like:
aSetOfThings := []string{
"all strings",
"another string",
"i guess i'll just pass strings",
}
Strings is simplistic, however, so let’s collect a set of a custom type:
type Person struct {
name string
age int
}
And create a slice
of persons:
people := []Person{
{
name: "jane",
age: 25,
}, {
name: "john",
age: 30
}, {
name: "jill",
age: 35,
}, {
name: "jeff",
age: 40,
}
}
This is useful, but is nothing like our original array
in JavaScript. Lets
add an interface so we can have a slice
of similar but different objects:
type Walker interface {
Walk()
}
// and ensure our Person implements
type Person struct {
name string
age int
}
func(p Person) Walk() {
fmt.Prinf("%s is walking\n", p.name)
}
func(p Person) Talk() {
fmt.Prinf("%s is talking\n", p.name)
}
// and add another struct that implements
type Dog struct {
name string
}
func(d Dog) Walk() {
fmt.Prinf("%s is walking\n", p.name)
}
func(d Dog) Fetch() {
fmt.Prinf("%s is fetching\n", p.name)
}
// and now we can have a slice of walkers, which are different objects,
// but comply with the same interface.
walkers := []Walker{
Person{name: "jill", age: 35},
Dog{name: "Fido"},
Person{name: "jack", age: 40}
}
Even though the structs are different (Talk()
vs Fetch()
), they both
fulfill the interface, so Golang
is happy.
But we can go further.
Lots of things Bounce
, so lets transition to a Bouncer
interface.
type Bouncer interface {
Bounce()
}
And lets add a func that takes a Bouncer
:
func BounceIt(b Bouncer) {
b.Bounce()
}
We want to send lots of things to our BounceIt
func.
Now, the first thing that bounces is likely a Ball
:
type Ball struct {
Radius int
Material string
}
func(b Ball) Bounce() {
fmt.Printf("Bouncing ball %+v\n", b)
}
We can directly bounce it, or let our bouncer do the bouncing:
b := Ball{}
b.Bounce()
BounceIt(b)
Nothing crazy yet. Now, let’s add an additional bouncing thing, but
increase its complexity a bit. The new one will embed
a separate
Bouncer
:
type Football struct {
Bouncer
Radius int
}
Which we can create by using our original Ball
:
b := Ball{Radius: 5, Material: "leather" }
fb := Football{Bouncer: Ball{}, Radius: 5}
b.Bounce()
fb.Bounce()
fbf.Bouncer.Bounce() // call it either way.
BounceIt(b)
BounceIt(fb)
Nothing too crazy here, our new struct
still implements the interface.
Let’s add another Bouncer
, that does something a little bit different:
// embed a pointer to a Ball
type Soccerball struct {
*Ball
}
func(sb Soccerball) Bounce() {
fmt.Printf("Bouncing ball %+v\n", sb)
}
This is still fine, the Interface
is a contract, it doesn’t care about
the implementation details:
b := Ball{}
f := Football{Ball{}, 5}
sb := Soccerball{&Ball{}}
b.Bounce()
f.Bounce()
f.Bouncer.Bounce() // call it either way.
sb.Bounce()
sb.Ball.Bounce()
BounceIt(b)
BounceIt(f)
BounceIt(sb)
And we might as well add some other things:
// simple, implements Bouncer itself, nothing embedded
type Jelly struct {
Radius int
}
func(j Jelly) Bounce() {
fmt.Printf("Bouncing jelly %+v\n", j)
}
// Rabbit method will use a pointer receiver, just to change
// things up a little bit more
type Rabbit struct {
Color string
}
// But with a pointer receiver on the method.
// still fine because the method signature doesn't change.
func(r *Rabbit) Bounce() {
fmt.Printf("Bouncing rabbit %+v\n", r)
}
And usage:
b := Ball{}
f := Football{Ball{}, 5}
sb := Soccerball{&Ball{}}
j := Jelly{}
// aha! since the method has a pointer receiver, we need to
// take the address of rabbit in order to use it.
r := &Rabbit{}
b.Bounce()
f.Bounce()
f.Bouncer.Bounce() // call it either way.
sb.Bounce()
sb.Ball.Bounce()
j.Bounce()
r.Bounce()
BounceIt(b)
BounceIt(f)
BounceIt(sb)
BounceIt(j)
BounceIt(r)
Finally, lets get as close as we can to the flexibility of the original
array example provided in JavaScript. Let’s add a few non-struct
objects,
starting with an int
:
type BouncyInt int
func(bi BouncyInt) Bounce() {
fmt.Printf("bouncing int? %+v\n", bi)
}
And instantiate:
// note the different instantiation rules for this one
var bi BouncyInt // implicit zero value
bi2 := BouncyInt(0) // explicit zero value
bi3 := BouncyInt{} // NO! BouncyInt is not a struct, despite the methods
bi.Bounce()
BounceIt(bi)
And we might as well have a bouncy func as well:
// we need a type for our func
type BouncyFunc func()
// and we can add the methods that will implement the Bouncer interface
func(bf BouncyFunc) Bounce() {
fmt.Printf("Bouncing func! %+v\n", bf)
}
Instantiate:
// need to cast our func
f := BouncyFunc(func() {})
f.Bounce()
BounceIt(f)
And for fun, we might as well make an adapter for this stubborn struct that refuses to bounce:
// this won't bounce
type NotBouncer interface {
Sit()
}
// and this will impl the NotBouncer interface
type DoesntBounce struct {}
func (db DoesntBounce) Sit() {
fmt.Printf("sit, don't bounce %+v\n", db)
}
// this isn't gonna bounce
nope := DoesntBounce{}
nope.Sit()
nope.Bounce() // error!
Until we wrap it:
type ForceItToBounce struct {
Bouncer
// we can embed it and MAKE it bounce
sits NotBouncer
}
// well, it still sits, but it will respond to bounce...
func (fitb *ForceItToBounce) Bounce() {
fitb.sits.Sit()
}
and:
fitb := ForceItToBounce{
sits: DoesntBounce{},
}
fitb.Bounce()
// note this method was a pointer receiver
BounceIt(&fitb)
Finally, might as well implement a Bouncer
generator:
// a helper to make BouncyFuncs
func getBouncyFunc() BouncyFunc {
return BouncyFunc(func() {})
}
// a func to return a func, will encapsulate our strange list of Bouncers
func GetBouncer() func() Bouncer {
// gotta cycle the seed to stir up the randomness
rand.Seed(time.Now().UnixNano())
// huh, the BouncingInt can be a bouncer...
var bouncingIntegerOfCraziness BouncyInt
// make a list of things we can return that impl the interface
bouncers := []Bouncer{
Ball{},
Soccerball{},
Football{},
Jelly{},
// watch the pointers
&Rabbit{},
// we proved ints are fine
bouncingIntegerOfCraziness,
// another one, inline
BouncyInt(0),
// manually make one
BouncyFunc(func() {}),
// call the helper
getBouncyFunc(),
// and a forced bouncer
ForceItToBounce{
sits: DoesntBounce{},
}
}
max := len(bouncers)
return func() Bouncer {
random := rand.Intn(max)
// fmt.Printf("%v \n", random)
return bouncers[random]
}
}
And then use this thing:
func main() {
bouncer := GetBouncer()
// lets get 10 bouncers
for i := 0; i < 10; i++ {
randBouncingThing := bouncer()
// and of course see if they bounce :)
randBouncingThing.Bounce()
BounceIt(randBouncingThing)
fmt.Printf("%T - %v\n", randBouncingThing, randBouncingThing)
}
}
Feel free to give the above a copy paste to Go Playground.
Initially, it felt like a huge loss of flexibility to move from
JavaScript into Golang. However, the above proves that Golang can
give us quite a bit of flexibility, while also providing type safety.
I’ve done enough refactoring of Go code now to realize the value of
the contracts provided with strict types and interfaces, and have not
yet missed too many of the crazy schenanigans of JavaScript.