Learning Go (Day 2)

My education in the Go Programming Language was was delayed by a few weeks due to some adventures in Iceland (more about that soon!), but I'm back on track now! So, here are my thoughts and updates after Day 2.

Practice Repository

My virtual machine died after I got back from Iceland, and its most recent snapshot was taken before I started learning Go. So, this meant that all of the Go code that I'd written so far was vaporized. D'oh!

I created a new practice repo and pushed it to GitHub to avoid accidental losses in the future. So, go here if you'd like to watch me take my first baby steps:

https://github.com/briandamaged/golang-practice

Methods and self

Methods do not reserve a specific variable name for this or self. Instead, the developer is responsible for naming the "receiver" variable. Okay, fine. Whatever.

More interestingly, the receiver can be a pointer or a value. Off the top of my head, I can really only think of 2 uses for this:

  1. It creates an easy way to rollback destructive calculations over a struct (instead of creating a temporary copy manually).
  2. It could allow for methods to be defined on primitive types.

Except... this 2nd item doesn't work quite the way you might expect.

Methods on primitives... sorta

You cannot define methods directly on primitives in Go. So, this will not work:

// Compiler error :(
func (x int) Double() int {
	return x * 2
}

However, you can define methods on primitives if you give the primitive a type alias first. So, this will work:

type MyInt int

func (x MyInt) Double() MyInt {
	return x * 2
}

I'm guessing that this is a way to avoid name collisions across packages???

More Generics, Plz!

Generics were introduced into Go relatively recently, and it appears that they are still largely unused throughout much of the core API. But, it looks like they could certainly help to cleanup the language a bit! For example, sorting a slice in a non-destructive manner is currently pretty rough:

func uglySort() {
	var digits = []int{8, 6, 7, 5, 3, 0, 9}

	// Create a copy of `digits`. Gotta initialize it
	// with the correct length, or else the `copy(..)`
	// function won't copy everything!
	var sorted = make([]int, len(digits))
	copy(sorted, digits)

	// This function cleverly sorts the slice in place
	// without relying upon Generics, but it's a little
	// awkward.
	sort.Slice(sorted, func(i, j int) bool {
		return sorted[i] < sorted[j]
	})

	fmt.Printf("Digits: %v\n", digits)
	fmt.Printf("Sorted: %v\n", sorted)
}

Bleh! 🤮

In contrast, here's what it could look like with Generics:

func prettySort() {
	var digits = []int{8, 6, 7, 5, 3, 0, 9}

	// such pretty! so amaze! wow!
	var sorted = SortedCopy(digits, func(x, y int) bool {
		return x < y
	})

	fmt.Printf("Digits: %v\n", digits)
	fmt.Printf("Sorted: %v\n", sorted)
}

The SortedCopy could be defined as follows:

type LessFunc[T any] func(x, y T) bool

func SortedCopy[T any](src []T, less LessFunc[T]) []T {
	// Create a copy
	var sorted = make([]T, len(src))
	copy(sorted, src)

	// Sort it!
	sort.Slice(sorted, func(i, j int) bool {
		return less(sorted[i], sorted[j])
	})

	return sorted
}

So much cleaner! 🤩

Granted, this Generic version is much less memory-efficient because it creates and discards several copies of each item. But, it's certainly a lot easier to use!

Methods and Generics

As of today, Methods only support Generics on the Receiver. This means that you cannot define additional Generics for the parameters and return values. Unfortunately, this means that there isn't a way to chain together functional-programming methods like you can in Ruby. For example:

// This is currently impossible because the compiler cannot keep
// track of the type information across method calls. :(
beginChain(digits).Reject(EvenNumbers).Map(ToHex).Reverse().Result()

I really hope they address this in a future version of the language because this is one of my favorite programming techniques!

Interfaces are implicit

Go introduced much of the world to the idea of "implicit interfaces". Importantly, this eliminates the need to declare that a type implements an interface. Instead, a type is automatically compatible with an interface as long as it has all of the required methods. Fancy! 🧐

Of course, I'm sure this concept was a lot more novel back when Go was first released. Today, you can find something very similar in both Python and TypeScript.

Interfaces and nil

It can be tricky to determine if an interface is actually nil. For example, consider the following:

type MyInt int

type Foo interface {
	CheckNil() bool
}

func (v *MyInt) CheckNil() bool {
	return v == nil
}

func evaluate(v Foo) {
	fmt.Printf("Is the interface nil? %t\n", v == nil)
	fmt.Printf("CheckNil says:        %t\v", v.CheckNil())
	fmt.Println()
}

func main() {
	var x MyInt = 3
	var y *MyInt

	fmt.Println("Assigning non-nil pointer to z")
	var z Foo = &x
	evaluate(z)

	fmt.Println("Assigning nil pointer to z")
	z = y
	evaluate(z)
}

You might be surprised to learn that the output from this program is:

Assigning non-nil pointer to z
Is the interface nil? false
CheckNil says:        false

Assigning nil pointer to z
Is the interface nil? false
CheckNil says:        true

So... why isn't the interface nil? This is because the interface is comprised of 2 fields:

  • A pointer to the data
  • A pointer to the "type"

When we assigned y to z, the compiler updated z's "type" to match y's type. Thus, the "type" field is not nil, and therefore the interface as a whole is not nil.

So... what exactly is this "type" thingy? I'm guessing that it's essentially a method lookup table, similar to a v_ptr in C++. Then again, it might also bundle additional information that is necessary for reflection operations. I'm sure I'll find out more details sooner or later.

Interfaces are Comparable... or are they?

The Go compiler assumes that interfaces are comparable. More specifically, interfaces are "equal" if:

  1. They have the same "type"
  2. Their data is also equal.

So, the following works as you might expect:

type Oops any

func main() {
	var x Oops = 3
	var y Oops = 3

	// Prints "true"
	fmt.Println(x == y)
}

Weirdly, the following code still compiles, but it generates a runtime panic because the []int datatype is not comparable:

type Oops any

func main() {
	var x Oops = []int{1, 2, 3}
	var y Oops = []int{1, 2, 3}

	// PANIC!!!
	fmt.Println(x == y)
}

I'm not entirely sure how I feel about this. On one hand, it's convenient for the compiler to assume that interfaces are comparable because that will often be the case. On the other hand, this assumption is dangerous, and therefore the compiler should demand some kind of a safety check first, such as a guarded block of code. Hypothetically:

type Oops any

func main() {
	var x Oops = []int{1, 2, 3}
	var y Oops = []int{1, 2, 3}

	// Compiler should disallow this
	fmt.Println(x == y)

	if TheseAreAllComparable(x, y) {
		// Compiler knows that the variables are comparable
		// now, so it allows the comparison to proceed.
		fmt.Println(x == y)
	}
}

Conclusions

Alright -- this blog post has helped me come to terms with a few more of Go's quirks. It's time to dive into Day 3!