Untyped Constants In Go

02 May 2020 | categories: blog

prev: EC2 VPN script | next: Shell Startup Files

For the majority of the time in my life that I’ve been programming I’ve worked with interpreted dynamically typed languages like Python or Ruby. I tried to learn some C about a year after setting out to learn how to code but I didn’t know enough then about how computers worked to fully grasp the concepts. Trying to learn C was my first exposure to a compiled statically typed language.

dynamic and static

Real quick, a dynamically typed programming language is a language where you don’t need to specify what type of value is being held in a variable. You can put anything you want in a variable and the interpreter won’t complain.

my_variable = {key: 'value'}
my_variable = 1234
my_variable = Object.new      # All of this is completel legal

In a statically typed language you need to say up front what the variable will be holding, and this type cannot hold anything other than a value of that type.

var myVariable string

myVariable = "hi there"    // legal
myVariable = 1234          // illegal, this code will not compile

There are pros and cons to these different approaches, I won’t get into that in this post. I just wanted to point out that in a typed language like Go you can’t just put any old value in a variable. There are rules.

types in Go

Go has many types defined in its spec and as you would expect it has all the usual primitives such as string, int, float64 etc.

As in the Go example above you can define a variable and its type before assigning a value to it, you can also use the shorthand notation and have Go infer the type from the value you pass in.

x := "hi"      // this syntax is the same as the next two lines

var y string
y = "hi"

This all makes sense, a string is a string right? What if we were to do the same with an integer which comes in many different flavours? If we let Go infer the type, what type would the variable hold?

var i int64
i = 1       // legal, the type of i is int64

j := 1      // also legal, except now the type is int (trust me)

How come assigning the same value, just in different ways, gives us two different types? If you hadn’t twigged on by now the reason is constant expressions in Go are untyped. The type is inferred by the compiler at compile time.

untyped in a typed world

Constants, specifically constant expressions, like strings and numbers are interesting because although they are untyped they do have a default type. For example integers have the default type of int and floats have the default type float64. In fact here are all the default types in Go: bool, rune, int, float64, complex128 or string. You can probably piece together which default types go with which constant, rune is Go’s version of a character.

Although constants have their own default type you can set a type for a constant as long as it makes sense to do so.

type MyInt int16

var i int64 = 3.0
var j float32 = 7
var k MyInt = 9

All of the above are perfectly legal, i is of type int64 and holds the value 3, j is of type float32 and holds the value 7.0, and k is of type MyInt and holds the value 9. The last example there works because MyInt has an underlying type int16 that makes sense with an integer constant. Trying to set k to something like a bool would not compile.

watch you don’t trip

Understanding the untyped nature of constants in Go really helps you avoid common pitfalls such as the following.

func main() {
  fmt.Printf("Gimme %s", getDuration(3))
}

func getDuration(sec int) time.Duration {
  return time.Second * sec
}

This code will fail to compile, whereas the following is perfectly legal.

func main() {
  fmt.Printf("Gimme %s", time.Second*3)
}

In the first example 3 is passed to getDuration and is assigned to the variable sec which is of type int, whereas the time package has defined a custom type Duration and time.Second is of type time.Duration. time.Duration has an underlying type of int64, however even if we were to change the parameter value’s type to int64 we would still run into the same problem.

The second example works precisely because 3 is an untyped constant expression, in situations like this the compiler sees time.Second * 3 and it knows the underlying type of time.Second is something 3 can be cast to. The compiler does its thing and you don’t need to worry.

Back to the first example, trying to use operands of two differing types like this is not allowed, but knowing it’s a type problem you have two ways you can get around it. You could either change the parameter type in the original function to time.Duration:

func main() {
  fmt.Printf("Gimme %s", getDuration(3))
}

func getDuration(sec time.Duration) time.Duration {
  return time.Second * sec
}

Or you can cast the argument to time.Duration

func main() {
  fmt.Printf("Gimme %s", getDuration(3))
}

func getDuration(sec int) time.Duration {
  return time.Second * time.Duration(sec)
}

losing precision

To finish up I want to stray slightly from the talk of default types and bring up casting seeing as I brought it up 5 seconds ago, more specifically how you can lose precision when casting. As always I’ll demonstrate it with a simple example.

You can cast a value to a type that has smaller precision, for example from int64 to int8

func main() {
  var i int64 = 1000000000000000001
  println(int8(i))
}

The above is fine, it will compile and run but only the number 1 will be printed out. An int64 has 64 bits it can use to represent a number, so the number 1,000,000,000,000,000,001 looks like this in binary:

00001101 11100000 10110110 10110011 10100111 01100100 00000000 00000001

int8 however can only hold 8 bits, so when you cast an int64 to int8 the higher order bits just get lopped off and you are left with this:

00000001

Which equals 1.

conclusion

Coming to a statically typed language from something like Ruby takes some getting used to, it can feel incredibly restrictive, I know I struggled a lot when I started.

In saying that I have come to really like the typed nature of Go, it’s like a safety blanket that stops you doing something stupid and generally catches a lot of bugs before you get anywhere near production.

Learn to love the types.

prev: EC2 VPN script | next: Shell Startup Files @skipcloud