Demystifying 'typechecking loop' errors in Go
A recent addition to the Pages product, Screens is a mobile store locator SDK with a server-side API written in Go. In keeping with our philosophy of full-stack teams, this gave me an opportunity to branch out from the mobile side of things and get my hands dirty with a brand new language.
Moving to a new programming language can be frustrating at times: trying to re-mold your brain into a new way of thinking, finding alien items of syntax just when you thought you were getting the hang of it, and dealing with compiler errors that don’t quite give enough of a hint to the newbie.
The Problem
Having only recently started working in Go, I fell afoul of the third problem just yesterday, when I saw the following during a build:
typechecking loop involving cache
The offending line being:
var cache *cache.Cache = cache.New(...)
The Cause
When trying to evaluate the problem line, the compiler gets confused as to the
difference between the variable named cache
and the package named
cache
. While this could potentially make for less readable code, in many cases
it wouldn’t cause an error like this. In fact, this particular error only
appeared when the cache
variable was declared in package scope.
As a result, there’s not a whole lot of help available regarding this error when it does crop up, and a number of cases where it used to appear have long since had more friendly messages put in place.
Unlike other languages in the C tradition, in Go the order in which package level variables are declared doesn’t actually matter. This means you can do things like this:
type Foo struct {
bar string
}
var foo *Foo = &Foo{bar: myBar}
var myBar = "Yep, this is totally OK"
This is a pretty neat feature, but in our broken program, this means that the
compiler thinks that cache.New(...)
refers to the cache variable that we’re
declaring in the same line, not the cache package. So the compiler expects that
cache should have the same type as itself, and we have our typechecking loop.
Solutions
The solution is pretty straightforward, just change the name of the cache
variable:
var myCache *cache.Cache = cache.New(...)
Alternatively, you could alias the package import:
import c "github.com/robfig/go-cache"
var myCache *c.Cache = c.New(...)
Aliasing the package can make the code more difficult to follow, so we went with the first approach.
More Complex Examples
Diving into the source code for go, a quick search for “typechecking loop” brings up gc.typecheck, which errors out if a node appears in its own typechecking chain (meaning that the node can only be typechecked by typechecking itself). So in addition to naming clashes, we would also encounter a loop when we have circular references.
Let’s have a look at this issue for the circular reference case. The example in the issue wraps the problem variables in arrays, but that’s not actually necessary to trigger the error, so we can reduce it to the below:
package main
import "fmt"
type Foo struct {
pbar *Bar
}
type Bar struct {
pfoo *Foo
}
var b = Bar{pfoo: &f}
var f = Foo{pbar: &b}
func main() {
fmt.Println(f, b)
}
This results in a typechecking loop because we need to know the type of f in order to check the type of b, and we need to know the type of b to check the type of f. If we try to be explicit about the type of b and f:
var b *Bar = Bar{pfoo: &f}
var f *Foo = Foo{pbar: &b}
We get an “initialization loop” error instead, which makes sense, since to
initialize b
, we need f
and vice versa.
Conclusions
Should you encounter this error message in the wild, checking for naming clashes is a good first step. Beyond that, try drawing a graph of references from the node mentioned in the error, and see if you can find a cycle.