2022.01.01

2022.01

A little bit of go

Interfaces and Receivers

We have gone all in on Go at Mash for our services and infrastructure. Go appears to have learned a lot from the past and has been a joy to work with. For just a ton of stuff, Go is very predictable and does what you expect it too. It has been easy for us to pick it up and start developing. Not sure how long it would have taken us to get this comfortable in say, Scala. Would probably be reviewing someones crazy implicits PR right now instead of writing this.

With that said, I have tripped on the Interface and Receiver interplay a lot, so going to write it down.

Go interfaces use duck-typing, if an object acts like a type, its that type. This means objects don’t have to explicitly declare that they implement and interface. This is a cool feature because it allows interface producers and consumers to divvy up responsibilities a little better. Instead of producers having to declare an interface and the implementations, they only have to handle the implementations. Consumers now get to declare the exact interface they need instead of having to use whatever the producer exposed. This allows a consumer to limit its dependencies and makes it easier to test and maintain.

In Go, objects implement an interface by adding all the required functions of an interface. These functions are called “receivers” I guess since they receive an instance of the object which holds state. There are two categories of receivers: value receivers and pointer receivers. Value receivers are receivers on value types and they receive a copy of the instance. Unsurprisingly, pointer receivers are receivers on pointer types and they receive a pointer to an instance.

Go has some syntactic sugar which auto-references or auto-dereferences a type in order to call a receiver. If there is a receiver foo on type A, but you are holding a reference to type *A (a pointer to type A) and you call foo on it, it will still work. This is the magic.

type Foo foo 

// value receiver
func (f Foo) value() {
  fmt.Println("value")	
}

// pointer receiver
func (f *Foo) pointer() {
  fmt.Println("pointer")	
}

func main() {
	x := Foo{}
	y := &x

	// straight up
	x.value()
	y.pointer()

	// auto reference
	x.pointer()
	(&x).pointer()

	// auto dereference
	y.value()
	(*y).value()
}

sugar in action

The above program compiles and outputs the following.

value
pointer
pointer
pointer
value
value

proof of magic

Things get weird though when you start working with interface types instead of just the implementations. Checkout what happens when instead of holding a reference to an implementation, Bar in this case, you hold a reference to an interface, Foo. This is a common scenario due to the pattern described above where consumers declare (and operate on) their dependency interfaces.

type Foo interface {
    foo()
}

type Bar struct {}

// value receiver
func (b Bar) foo() {}

func main() {
    // works
    var foo Foo = &Bar{}
    foo.foo()

    // works
    var foo Foo = Bar{}
    foo.foo()
}

a value receiver auto-referencing magic still works

type Foo interface {
    foo()
}

type Bar struct {}

// pointer receiver
func (b *Bar) foo() {}

func main() {
    // works
    var foo Foo = &Bar{}
    foo.foo()

    // DOES NOT WORK
    var foo Foo = Bar{}
    foo.foo()
}

UH OH, a pointer receiver does not compile

For the pointer receiver the compiler spits out Bar does not implement Foo (foo method has pointer receiver). The fact that the magic doesn’t work in all cases makes me wonder if the magic was a bad call, but in any case, why is it broken?

The tl;dr is the concrete value stored in an interface is not addressable.

The interface type does not have enough information to create a pointer to whatever is in the interface. This makes more sense thinking about the end requirement. To call foo(), there needs to be a reference to a *Bar to pass to it. An interface doesn’t know if it is satisfied by Bar or Baz, as long as they both implement the interface. So it can’t create a *Bar cause it might end up pointing at a Baz.

Not sure my take away here, might just prefer value receivers in all places to just avoid this.

Histograms are Dope

About a decade ago, to the engineering team I was on. the engineering team I was on rolled out our next generation metrics platform because our old one could not handle the write load anymore. I don’t remember much about the platform, but I do remember a grizzled engineer’s presentation on Histograms.

A buddy recently stumbled upon an old PR of mine from another histogram project and coincidentally, I just rolled out another metrics platform at the new gig. The common factor of these three projects is me re-discovering every time “Oh dang, histograms are pretty dope”.

They are pretty clever.

The most straight forward way to monitor an application is to record the latency of every request. It is easy to crunch these numbers and figure out things like the 50th percentile or diagnose times which had a lot of slow requests. For a popular app though, this can quickly become a lot of data. And the reality is we don’t care about most of it. The questions we want answers too are not on the granularity of every request, they are higher level like “is this app healthy?”.

Histograms are a trade-off between granularity and space. They are a lot less granular, but way more space efficient which is perfect for common monitoring use cases. A histogram has pre-defined buckets for values. In our latency case, we would make buckets on milliseconds (e.g. 0-49ms, 50-99ms, 100+ms). Instead of storing every request’s data, we just plop it in a bucket. All the request data points are boiled down to a small map, but the map can still answer our questions like “is this app healthy?”.

If my cycle continues, in about a year I’ll forget how histograms work and get to have my “dang that’s cool” moment again.

Language Server Protocols

As a developer, I have a pretty close relationship with my IDE. Back in the day my journey started with the lightweight Vim. I then moved to the heavier Eclipse and Intellij at the Java shops. Intellij is very heavy on the system resources, but buys a developer a lot (especially with static languages) in productivity. Generally, the resource heavy IDE’s bring information that is discovered at compile-time to a developer much faster, maybe even milliseconds after they write some code. Faster feedback loops are very valuable, definitely worth the extra memory/CPU usage.

Over the past year I have used VSCode almost exclusively. It provides the same quick feedback as Intellij, but also has a snappier and cleaner user interface. And where Intellij’s strength is really focused in JVM languages, VSCode has strong support for tons of languages. It is able to do this by embracing a new IDE pattern called “Language Server Protocols” where the responsibilities of an IDE are busted up. Instead of an IDE having to handle diagnosing and showing issues to a developer, it offloads the diagnosing to a separate process. These processes are language specific and define a protocol which allows an IDE to ask questions like “Are there bugs in this Go file?”.

VSCode’s weakness is that it uses Electron under the hood, so it still claims a ton of my system resources despite offloading work to LSP processes. Also, despite Electron’s goal of being a platform for applications to develop one, its a pretty shitty platform in linux-land with a near constant stream of little bugs.

So here I am, back to using the lightweight Vim (specifically a fork called neovim). But, the big difference these days is that it supports LSPs! So I get the best of both worlds: the fast feedback loops with the lightweight user interface.