Constants

In Golang, we use the term constant to represent fixed (unchanging) values such as 5, 1.34, true, "Hello" etc.

Literals are constants

All the literals in Golang, be it integer literals like 5, 1000, or floating-point literals like 4.76, 1.89, or boolean literals like true, false, or string literals like "Hello", "John" are constants.

Constants Examples
integer constants 1000, 67413
floating-point constants 4.56, 128.372
boolean constants true, false
rune constants 'C', 'ä'
complex constants 2.7i, 3 + 5i
string constants "Hello", "Rajeev"

Declaring a Constant

Literals are constants without a name. To declare a constant and give it a name, you can use the const keyword like so -

const myFavLanguage = "Python"
const sunRisesInTheEast = true

You can also specify a type in the declaration like this -

const a int = 1234
const b string = "Hi"

Multiple declarations in a single statement is also possible -

const country, code = "India", 91

const (
	employeeId string = "E101"
	salary float64 = 50000.0
)

Constants, as you would expect, cannot be changed. That is, you cannot re-assign a constant to a different value after it is initialized -

const a = 123
a = 321 // Compiler Error (Cannot assign to constant)

Typed and Untyped Constants

Constants in golang are special. They work differently from how they work in other languages. To understand why they are special and how they exactly work, we need some background on Go’s type system. So let’s jump right into it -

Background

Go is a statically typed programming language. Which means that the type of every variable is known or inferred by the compiler at compile time.

But it goes a step further with its type system and doesn’t even allow you to perform operations that mix numeric types. For example, You cannot add a float64 variable to an int, or even an int64 variable to an int-

var myFloat float64 = 21.54
var myInt int = 562
var myInt64 int64 = 120

var res1 = myFloat + myInt  // Not Allowed (Compiler Error)
var res2 = myInt + myInt64  // Not Allowed (Compiler Error)

For the above operations to work, you’ll need to explicitly cast the variables so that all of them are of the same type -

var res1 = myFloat + float64(myInt)  // Works
var res2 = myInt + int(myInt64)      // Works

If you’ve worked with other statically typed languages like C, C++ or Java, then you must be aware that they automatically convert smaller types to larger types whenever you mix them in any operation. For example, int can be automatically converted to long, float or double.

So the obvious question is that - why doesn’t Go do the same? why doesn’t it perform implicit type conversions like C, C++ or Java?

And here is what Go designers have to say about this (Quoting from Golang’s official doc) -

The convenience of automatic conversion between numeric types in C is outweighed by the confusion it causes. When is an expression unsigned? How big is the value? Does it overflow? Is the result portable, independent of the machine on which it executes? It also complicates the compiler; “the usual arithmetic conversions” are not easy to implement and inconsistent across architectures. For reasons of portability, we decided to make things clear and straightforward at the cost of some explicit conversions in the code. (Excerpt from Golang’s official doc)

All right! So Go doesn’t provide implicit type conversions and it requires us to do explicit type casting whenever we mix variables of multiple types in an operation.

But how does Go’s type system work with constants? Given that all of the following statements are valid in Golang -

var myInt32 int32 = 10    
var myInt int = 10
var myFloat64 float64 = 10
var myComplex complex64 = 10

What is the type of the constant value 10 in the above examples? Moreover, if there are no implicit type conversions in Golang, then wouldn’t we need to write the above statements like -

var myInt32 int32 = int32(10)
var myFloat64 float64 = float64(10)
// etc..

Well, the answers to all theses questions lay in the way constants are handled in Golang. So let’s find out how they are handled.

Untyped Constants

Any constant in golang, named or unnamed, is untyped unless given a type explicitly. For example, all of the following constants are untyped -

1       // untyped integer constant
4.5     // untyped floating-point constant
true    // untyped boolean constant
"Hello" // untyped string constant

They are untyped even after you give them a name -

const a = 1
const f = 4.5
const b = true
const s = "Hello"

Now, you might be wondering that I’m using terms like integer constant, string constant, and then I’m also saying that they are untyped.

Well yes, the value 1 is an integer, 4.5 is a float, and "Hello" is a string, but they are just values. They are not given a fixed type yet, like int32 or float64 or string, that would force them to obey Go’s strict type rules.

The fact that the value 1 is untyped allows us to assign it to any variable whose type is compatible with integers -

var myInt int = 1
var myFloat float64 = 1
var myComplex complex64 = 1

Note that, Although the value 1 is untyped, it is an untyped integer. So it can only be used where an integer is allowed. You cannot assign it to a string or a boolean variable for example.

Similarly, an untyped floating-point constant like 4.5 can be used anywhere a floating-point value is allowed -

var myFloat32 float32 = 4.5
var myComplex64 complex64 = 4.5

Let’s now see an example of an untyped string constant.

In Golang, you can create a type alias using the type keyword like so-

type RichString string  // Type alias of `string`

Given the strongly typed nature of Golang, you can’t assign a string variable to a RichString variable-

var myString string = "Hello"
var myRichString RichString = myString // Won't work.

But, you can assign an untyped string constant to a RichString variable because it is compatible with strings -

const myUntypedString = "Hello"
var myRichString RichString = myUntypedString  // Works

Constants and Type inference: Default Type

Go supports type inference. That is, it can infer the type of a variable from the value that is used to initialize it. So you can declare a variable with an initial value, but without any type information, and Go will automatically determine the type -

var a = 5  // Go compiler infers the type of variable `a`

But how does it work? Given that constants in Golang are untyped, what will be the type of the variable a in the above example? Will it int8 or int16 or int32 or int64 or int?

Well, it turns out that every untyped constant in Golang has a default type. The default type is used when we assign the constant to a variable, and the variable doesn’t have any explicit type available.

Following are the default types for various constants in Golang -

Constants Default Type
integers (10, 76) int
floats (3.14, 7.92) float64
complex numbers (3+5i) complex128
characters ('a', '♠') rune
booleans (true, false) bool
strings (“Hello”) string

So, in the above example var a = 5, since no explicit type information is available, the default type for integer constants is used to determine the type of a, which is int.

Typed Constants

In Golang, Constants are typed when you explicitly specify the type in the declaration like this-

const typedInt int = 1  // Typed constant

Just like variables, all the rules of Go’s type system applies to typed constant. For example, you cannot assign a typed integer constant to a float variable -

var myFloat64 float64 = typedInt  // Compiler Error

With typed constants, you lose all the flexibility that comes with untyped constants like assigning them to any variable of compatible type or mixing them in mathematical operations. So you should declare a type for a constant only if it’s absolutely necessary. Otherwise, just declare constants without a type.

Numeric Expressions involving constants

You can mix and match untyped numeric constants in any numeric expression freely. They will just work without any problem-

package main
import "fmt"

func main() {
  var result = 7.5/5
  fmt.Printf("result is %v which is of type %T\n", result2, result2)
}
# Output
result is 1.5 which is of type float64

In the above example, 7.5 is an untyped floating-point constant and 5 is an untyped integer constant. The result is 1.5, an untyped floating-point constant, whose default type is float64. Therefore the type of the variable result is float64.

Let’s see another example -

package main
import "fmt"

func main() {
  var result = 25/2
  fmt.Printf("result is %v which is of type %T\n", result2, result2)
}

Can you guess the result of the above program?

Well, If we calculate the result as we do in mathematics, the output should be 12.5. But the actual output of the program is 12.

Note that, both 25 and 2 are untyped integer constants and therefore the result is truncated to an untyped integer 12.

To get the correct result, you can do one of the following:

// Use a float value in numerator or denominator
var result = 25.0/2
// Explicitly cast the numerator or the denominator
var result = float64(25)/2

One more example -

package main
import "fmt"

func main() {
	var result = 4.5 + (10 - 5) * (3 + 2)/2
	fmt.Println(result)
}

What will be the result of the above program?

Well, it’s not 17. The actual result of the above program is 16.5. Let’s go through the evaluation order of the expression to understand why the result is 16.5

4.5 + (10 - 5) * (3 + 2)/2
          ↓
  4.5 + (5) * (3 + 2)/2
          ↓
   4.5 + (5) * (5)/2
          ↓
     4.5 + (25)/2
          ↓
      4.5 + 12
          ↓
        16.5

You got it right? :)

To get the correct result, you can do one of the following:

// Use a float value in the numerator or denominator
var result = 4.5 + (10 - 5) * (3 + 2)/2.0
// Explicitly cast numerator or the denominator
var result = 4.5 + float64((10 - 5) * (3 + 2))/2

Conclusion

Untyped constants are an amazing design decision taken by Go creators. Although Go has a strong type system, You can use untyped constants to escape from Go’s type system and have more flexibility when working with mixed data types in any expression.

As always, Thanks for reading. Please share your feedback about this article, or ask any question that you might have in the comment section below.

Next Article: Golang Control Flow Statements: If, Switch and For