Lately I started to get that excellent sense of flow state while working on my Golang project.

The flow state of programming is such a nice feeling, when it happens. I don’t get to experience it as much as I used to, because so much of my work currently consists of meetings, plans, Slack messages, and things that don’t involve long periods of focus.

And when I first started writing Go for this project, I found it a little tedious. Go has a deliberately limited vocabulary and a lot of boilerplate error handling.

But after a few solid days, I started to really enjoy writing Go. It’s not as elegant as Swift (the other strongly typed language I’ve played with lately). But it doesn’t have the horrible type inference slowdowns either. It’s more like writing C, but with a much better standard library, fewer semicolons, and a better DX.

What’s good about writing C, and also about writing Go, is this: It’s all functions and structs. The building blocks are very uniform. If you can write a good function, with a sensible name, and compose it nicely with other functions, using sensible data structures, then the program almost starts to write itself. It has a kind of minimalist harmony.

The flow is mostly this: a flow of writing functions, specifying data structures, extracting shared functionality, arranging a project into sensible packages, and testing.

And quite often, if my Go program can compile, then the behavior is fundamentally correct out of the box. I do write tests too, and I test interactively. But it really improves flow state and productivity to know that, if my code compiles, then there’s an excellent chance it’s already in working order.

I started to ignore the boilerplate, too. I used to be annoyed by all the if err != nil blocks, but lately I just … don’t pay them much attention.

Notes on using Copilot

It’s the first time I’ve ever used Copilot with a new programming language.

I’m more excited currently about the code autocompletion than about the Copilot chat. The chat has lackluster performance and a bad UX for applying changes. The autocompletion is quite good and saves me a bunch of typing. It tends to be fast at noticing common patterns and wanting to apply them. It’s sometimes incorrect, of course, but I always re-read and edit the output as soon as I hit tab.

Sometimes I use the Copilot chat to basically substitute for things I would Google. “How do you unmashal a blob of YAML data into a slice of structs?” The result from Copilot is similar in quality to the result from StackOverflow. (Copilot is clearly less reliable than the excellent gobyexample.com, however.)

Sometimes it improves flow state to use Copilot, because you don’t have to leave your text editor.

Sometimes Copilot breaks flow state, though, by giving slow or unreliable answers. I’m torn about that part.

Things that break flow state

I did find a few strange things:

Failed type inferences

The Go tooling in VSCode is mostly great, but there’s an edge case where the Go language server can fail to infer return types correctly. In the case I stumbled into, my code would still compile and run correctly, but in VSCode, some files were wrongly marked as broken, as having invalid types in their function signatures.

Circular dependencies

You can’t have circular package dependencies, which makes sense. But what if package A depends on package B and C, and then package C wants to use a struct that’s defined in package B?

I understand it’s idiomatic to use interfaces in such circumstances, and then the interface definition can just be imported from some base package that has no dependencies. This avoids any circularities.

I was, however, surprised that Go’s interfaces don’t cover fields; they only cover methods. That feels a bit redundant if all you need is to define a few accessors.

This brings me to a related issue with interfaces.

Inconsistent calling semantics for interfaces

Suppose you have an interface for a Point and you have a struct that implements it:

type Point struct {
      X int
      Y int
}

func (p Point) s() string {
      return fmt.Sprintf("%d/%d", p.X, p.Y)
}

type PointInterface interface {
      s() string
}

This works:

func printPoint(p PointInterface) {
      fmt.Println(p.s())
}

func main() {
      p := Point{5, 2}
      printPoint(p)
}

This does not work:

func printList(list []PointInterface) {
      for i, _ := range list {
            fmt.Println(list[i].s())
      }
}

func main() {
      plist := []Point{{10, 20}, {20, 30}}

      printList(plist)
}

The error reads: cannot use plist (variable of type []Point) as []PointInterface value in argument to printList.

Basically, you pass a Point into a function that expects a PointInterface. But you can’t pass []Point to a function that expects a []PointInterface. Instead, you have to do a conversion step to manually transform a slice of structs into a slice of interfaces.

Per Stack Overflow, the explanation seems to be this: It’s an O(N) operation to convert a slice of structs into a slice of interfaces that the struct implements. And Go doesn’t want to do that automatically. But converting just one at a time is O(1) so the compiler will handle it silently.

I found this unintuitive.

Not my favorite ever package manager

There’s nothing seriously wrong with go mod, but I do not love it, either. I was confused by the way Go handles multiple modules inside a single project. I don’t really need multiple modules, as dividing my project into packages is plenty of structure for my use case; but I found it less than luminous.

A lot better than C: I’ll say that much.

In sum

It’s such a nice experience to seriously sit down with a new language at work. I wouldn’t use Go for everything. And the boilerplate is slightly exhausting. But it clearly has a sweet spot, and it seems very maintainable once you write it.

These are good things.