Introduction
One of the more unique features of Go is how the language implements constants. The rules for constants in the language specification are unique to Go. They provide the flexibility Go needs at the compiler level to make the code we write readable and intuitive while still maintaining a type safe language.
This post will attempt to build a foundation for what numeric constants are, how they behave in their simplest form and how best to talk about them. There are a lot of little nuances, terms and concepts that can trip us up. Because of this, the post is going to take things slowly.
So if you are ready to peek under the covers just a bit, roll up your sleeve and let’s get started:
Untyped and Typed Numeric Constants
Constants can be declared with or without a type in Go. When we declare literal values in our code, we are actually declaring constants that are both untyped and unnamed.
The following examples show typed and untyped numeric constants that are both named and unnamed:
const untypedInteger = 12345
const untypedFloatingPoint = 3.141592
const typedInteger int = 12345
const typedFloatingPoint float64 = 3.141592
The constants on the left hand side of the declaration are named constants and the literal values on the right hand side are unnamed constants.
Kinds of Numeric ConstantsYour first instinct may be to think that typed constants use the same type system as variables, but they don’t. Constants have their own implementation for representing the values that we associate with them. Every Go
compiler has the flexibility to implement constants as they wish, within a set of mandatory
requirements.
When declaring a typed constant, the declared type is used to associate the type’s precision limitations. It does not change how the value is being internally represented. Because the internal representation of constants can be different between the different compilers, it is best to think of constants as having a
kind, not a type.
Numeric constants can be one of four kinds: integer, floating-point, complex and rune:
12345 // kind: integer
3.141592 // kind: floating-point
1E6 // kind: floating-point
In the example above, we have declared three numeric constants, one of kind integer and two of kind floating-point. The form of the literal value will determine what kind the constant takes. When the form of the literal value contains a decimal or exponent, the constant is of kind floating-point. When the form does not contain a decimal or exponent, the constant is of kind integer.
Constants Are Mathematically ExactRegardless of the implementation, constants are always considered to be mathematically exact. This is something that makes constants in Go unique. This is not the case in other languages like C and C++.
Integers can always be represented precisely when there is enough memory to store their entire value. Since the specification requires integer constants to have at least 256 bits of precision, we are safe in saying integer constants are mathematically exact.
To have mathematically exact floating-point numbers, there are different strategies and options that the compiler can employ. The specification does not state how the compiler must do this, it just specifies a set of mandatory requirements that need to be met.
Here are two strategies that the different Go compilers use today to implement mathematically exact floating-point numbers:
- One strategy is to represent all floating-point numbers as fractions, and use rational arithmetic on those fractions. This is what go/types does today and these floating-point numbers never have any loss of precision.
- Another strategy is to use floating-point numbers with so much precision that they appear to be exact for all practical purposes. When we use floating-point numbers with several hundred bits, the difference between exact and approximate becomes virtually non-existent. This is what the gc/gccgo compilers do today.
As developers however, it is best to not consider what internal representation is being used by the compiler, it is irrelevant. Just remember that all constants, regardless if they are declared with or without a type, use the same representation to store their values, which is not the same as variables and is mathematically exact.
Mathematically Exact ExampleSince constants only exist during compilation, it is hard to provide an example that shows constants are mathematically exact. One way is to show how the compiler will let us declare constants of kind integer with values that are much larger than the largest integer types can support.
Here is a program that can be compiled because constants of kind integer are mathematically exact:
package main
import "fmt"
// Much larger value than int64.
const myConst = 9223372036854775808543522345
func main() {
fmt.Println("Will Compile")
}
If we change the constant to be of type
int64, which means the constant is now bound to the precision limitations of a 64 bit integer, the program will no longer compile:
package main
import "fmt"
// Much larger value than int64.
const myConst int64 = 9223372036854775808543522345
func main() {
fmt.Println("Will NOT Compile")
}
Compiler Error:
./ideal.go:6: constant 9223372036854775808543522345 overflows int64
Here we can see that constants of kind integer can represent very large numbers and why we say they are mathematically exact.
Numeric Constant DeclarationsWhen we declare an untyped numeric constant, there are no type constraints that must be met by the constant value:
const untypedInteger = 12345 // kind: integer
const untypedFloatingPoint = 3.141592 // kind: floating-point
In each case, the untyped constant on the left hand side of the declaration is given the same kind and value as the constant on the right.
When we declare a typed constant, the constant on the right hand side of the declaration must use a form that is compatible with the declared type on the left:
const typedInteger int = 12345 // kind: integer
const typedFloatingPoint float64 = 3.141592 // kind: floating-point
The value on the right hand side of the declaration must also fit into the range for the declared type. For instance, this numeric constant declaration is invalid:
const myUint8 uint8 = 1000
uint8 only can represent numbers from 0 to 255. This is what I mean when I said earlier that the declared type is used to associate the type’s precision limitations.
Implicit Integer Type ConversionsIn Go there are no implicit type
conversions between variables. However, implicit type conversions between variables and constants can happen regularly by the compiler.
Let’s start with an implicit integer conversion:
var myInt int = 123
In this example we have constant
123 of kind integer being implicitly converted to a value of type
int. Since the form of the constant is not using a decimal point or exponent, the constant takes the kind integer. Constants of kind integer can be implicitly converted into signed and unsigned integer variables of any length as long as no truncation needs to take place.
Constants of kind floating-point can also be implicitly converted into integer variables if the constant uses a form that is compatible with the integer type:
var myInt int = 123.0
We can also perform implicit type conversion assignments without declaring an explicit type for the variable:
var myInt = 123
In this case, the default type of
int64 is used to initialize the variable being assigned with constant
123 of kind integer.
Implicit Floating-Point Type ConversionsNext let’s look at an implicit floating-point conversion:
var myFloat float64 = 0.333
This time the compiler is performing an implicit conversion between constant
0.333 of kind floating-point to a variable of type
float64. Since the form of the constant is using a decimal point, the constant takes the kind floating-point. The default type for a variable initialized with a constant of kind floating-point is
float64.
The compiler can also perform implicit conversions between constants of kind integer to variables of type
float64:
var myFloat float64 = 1
In this example, constant
1 of kind integer is implicitly converted to a variable of type
float64.
Kind PromotionPerforming constant arithmetic between other constants and variables is something we do quite often in our programs. It follows the rules for
binary operators in the specification. The rule states that operand types must be identical unless the operation involves shifts or untyped constants.
Let’s look at an example of two constants that are multiplied together:
var answer = 3 * 0.333
In this example we perform multiplication between constant
3 of kind integer and constant
0.333 of kind floating-point.
There is a rule in the specification about constant expressions that is specific to this operation:
"Except for shift operation, if the operands of a binary operation are different kinds of untyped constants, ..., the result use the kind that appears later in this list: integer, rune, floating-point, complex."Based on this rule, the result of the multiplication between these two constants will be a constant of kind floating-point. Kind floating-point is being promoted ahead of kind integer based on the rule.
Numeric Constant ArithmeticLet’s continue with our multiplication example:
var answer = 3 * 0.333
The result of the multiplication will be a new constant of kind floating-point. That constant is then assigned to the variable
answer through an implicit type conversion from kind floating-point to
float64.
When we divide numeric constants, the kind of the constants determine how the division is performed.
const third = 1 / 3.0
When one of the two constants are of kind floating-point, the result of the division will also be a constant of kind floating-point. In our example we have used a decimal point to represent the constant in the denominator. This follows the rules for kind promotion that we talked about before.
Let’s take the same example but use kind integer in the denominator:
const zero = 1 / 3
This time we are performing division between two constants of kind integer. The result of the division will be a new constant of kind integer. Since dividing 3 into the value of 1 represents a number that is less than 1, the result of this division is constant
0 of kind integer.
Let’s create a typed constant using numeric constant arithmetic:
type Numbers int8
const One Numbers = 1
const Two = 2 * One
Here we declare a new type called
Numbers with a base type of
int8. Then we declare constant
One with type
Numbers and assign constant
1 of kind integer. Next we declare constant
Two which is promoted to type
Numbers through the multiplication of constant
2 of kind integer and constant
One of type
Numbers.
The declaration of constant
Two shows an example of a constant getting promoted not just to a user-defined type, but a user-defined type associated with a base type.
One Practical ExampleLet’s look at one practical example right from the standard library. The time package declares this type and set of constants:
type Duration int64
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
)
All of the constants declared above are constants of type
Duration which have a base type of
int64. Here we are declaring typed constants using constant arithmetic between constants that are typed and untyped.
Since the compiler will perform implicit conversions for constants, we can write code in Go like this:
package main
import (
"fmt"
"time"
)
const fiveSeconds = 5 * time.Second
func main() {
now := time.Now()
lessFiveNanoseconds := now.Add(-5)
lessFiveSeconds := now.Add(-fiveSeconds)
fmt.Printf("Now : %v\n", now)
fmt.Printf("Nano : %v\n", lessFiveNanoseconds)
fmt.Printf("Seconds : %v\n", lessFiveSeconds)
}
Output:
Now : 2014-03-27 13:30:49.111038384 -0400 EDT
Nano : 2014-03-27 13:30:49.111038379 -0400 EDT
Seconds : 2014-03-27 13:30:44.111038384 -0400 EDT
The power of constants are exhibited with the method calls to
Add. Let’s look at the definition of the
Add method for the receiver type
Time:
func (t Time) Add(d Duration) Time
The
Add method accepts a single parameter of type
Duration. Let’s look closer at the method calls to
Add from our program:
var lessFiveNanoseconds = now.Add(-5)
var lessFiveMinutes = now.Add(-fiveSeconds)
The compiler is implicitly converting constant
-5 into a variable of type
Duration to allow the method call to happen. Constant
fiveSeconds is already of type
Duration thanks to the rules for constant arithmetic:
const fiveSeconds = 5 * time.Second
The multiplication between constant
5 and
time.Second results in constant
fiveSeconds becoming a constant of type
Duration. This is because constant
time.Second is of type
Duration and this type is promoted when determining the type of the result. To support the function call, the constant still needs to be implicitly converted from a constant of type
Duration to a variable of type
Duration.
If constants didn't behave the way they do, these kind of assignments and function calls would always require explicit conversions. Look at what happens when we try to use a value of type
int to make the same method call:
var difference int = -5
var lessFiveNano = now.Add(difference)
Compiler Error:
./const.go:16: cannot use difference (type int) as type time.Duration in function argument
Once we use a typed integer value as the parameter for the
Add method call, we received a compiler error. The compiler will not allow implicit type conversions between typed variables. For that code to compile we would need to perform an explicit type conversion:
Add(time.Duration(difference))
Constants are the only mechanism we have to write code without the need to use explicit type conversions.
ConclusionWe take the behavior of constants for granted, which is a testament to the language designers and those who have worked hard on this feature. A lot of work and care has gone into making constants work this way and the benefits are hopefully clear.
So the next time you are working with a constant, remember you are working with something that is unique. A hidden gem buried in the compiler that doesn’t get enough credit or recognition as a unique feature of Go. Constants help make coding in Go fun and the code we write readable and intuitive. While at the same time keeping the code we write type safe.
ThanksThanks to
[Nate Finch](/broken-link) and
Kim Shrier who have provided several reviews of the post that have helped to make sure the content and examples were accurate, flowed well and would be interesting to Go developers. I was ready to give up a few times and Nate’s encouragement kept me going.
Special thanks to Robert Griesemer and others on the Go dev team for their time and patience in teaching me the subject matter. The Go dev team is filled with an amazing group of people who really care about the community and the people who are a part of it. Thanks!!