Chris's Wiki :: blog/programming/GoNoTypeForTypes


Over on r/golang, someone asked What's the point of limiting .(type) evaluations to type switches:

I know the Go feature set is very well thought out and I'm sure there's a good reason for it, but I'm curious why I can't do fmt.Println(x.(type))

(As pointed out in the replies, you can get the same information with fmt's %T formatting verb.)

Although there are a number of things going on here, one of them is that Go has opted not to make types visible as explicit entities in the language. In a language like Python, the type of things is explicitly exposed as a fully visible part of the language, through operations like type(). In Go, there's no such direct way of exposing the type of something and you can only compare it against other types through mechanisms like type switches.

(Python goes further and makes types pretty much as first class entities as functions are.)

Part of what this means is that in Go, you cannot write an expression like 'x := y.(type)' not just because the language syntax forbids it, but because there is no standard type that the variable x can be. If you wanted to allow this, you would have to create a new Go type and define what its behavior was.

Go does make type information accessible, but only through the reflect standard library package. There are two things about this. First, reflect isn't part of the language itself, so another implementation of Go would be theoretically free to leave it out (although a certain amount of code would have problems); TinyGo doesn't quite go that far, although it has a minimal version. Second, it's relatively clear that what you get from reflect and manipulate through it is not literally the type information from the language; instead, it is a reflect-created thing that may or may not actively reflect the underlying reality. The only nominal exceptions are reflect.SliceHeader and reflect.StringHeader, and reflect explicitly says that you can't use these safely.

(My personal guess is that part of why reflect provides them is so that if the string or slice header ever changes, code can break visibly because the reflect structure fields it's trying to use aren't there any more. This wouldn't happen if Go forced everyone to define their own private versions of these runtime structures; instead you'd just get silently corrupted results. And people do do things with these headers today.)

In general, not explicitly requiring Go the language to have types as explicit, visible entities preserves some implementation freedom. However, I believe that Go still does require that an implementation keeps around some more type information than you'd expect; for example, I don't believe it would be proper in general for a Go implementation to reduce everything from distinct named types to structural types after compiling the program. This is because if you have the following code, I believe the type cast is required to fail:

type fred int
var a fred [...]
b := interface{}(a)
i, ok := b.(int)

(You can construct a more elaborate example with a type switch with both fred and int as options.)

Structurally, fred and int are the same thing, but Go explicitly distinguishes between named types even if they are structurally identical. As seen here, this distinction can be recovered dynamically, which implies that it must be accessible to the runtime; the runtime needs to have some way to distinguish between a fred and an int that have been turned into interfaces.

(You can get similar situations if the two differently named types have different method sets and you are converting between different interfaces; here, one type might be convertible but the other not.)

PS: One reason a Go implementation might be interested in some degree of type erasure is to reduce the amount of additional type information that has to be carried around with the compiled binary. If the actual code never needs the information, why have it present at all? But clearly this would require whole-program analysis so you can tell whether or not these things are needed, and you'd probably have to explicitly not support things like fmt's %T verb.