For one thing you have C unions. You can store different types in the same place and use information stored elsewhere to know what the bits really mean. It’s horribly unsafe compared to a Go interface or the tagged unions in other languages, but uses less space.
Also, there is the NaN boxing trick [1], which allows you to store a value that is essentially a tagged union that is either a double, a pointer, or an integer, in the same space as a double. To get these in a safe language, they essentially have to be built in already.
Go’s interface type is very high overhead compared to the union types used in other languages because it uses an entire pointer as a tag. Though, it only matters in special cases like interpreters. Most performance-intensive code isn’t nearly so dynamic.
Yes, the problem is that you still need pointers, and furthermore, pointers to different kinds of objects, of different sizes. Also, Go’s garbage collector should know about the pointers.
You could represent the heap as an array, use array offsets instead of pointers, and do your own garbage collection, but that’s pretty low level and you might as well compile to WebAssembly.
Also, there is the NaN boxing trick [1], which allows you to store a value that is essentially a tagged union that is either a double, a pointer, or an integer, in the same space as a double. To get these in a safe language, they essentially have to be built in already.
Go’s interface type is very high overhead compared to the union types used in other languages because it uses an entire pointer as a tag. Though, it only matters in special cases like interpreters. Most performance-intensive code isn’t nearly so dynamic.
[1] https://sean.cm/a/nan-boxing