Working with Constants in Golang

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.

ConstantsExamples
integer constants1000, 67413
floating-point constants4.56, 128.372
boolean constantstrue, false
rune constants'C', 'ä'
complex constants2.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 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 automatically infers the type of the 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 be 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 that doesn’t have any explicit type available.

Following are the default types for various constants in Golang -

ConstantsDefault 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 statement 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.

Constant expressions

The fact that constants are untyped (unless given a type explicitly) allows you to mix them in any expression freely.

So you can have a contant expression containing a mix of various untyped constants as long as those untyped constants are compatible with each other -

const a = 5 + 7.5 // Valid
const b = 12/5    // Valid
const c = 'z' + 1 // Valid

const d = "Hey" + true // Invalid (untyped string constant and untyped boolean constant are not compatible with each other)

The evaluation of constant expressions and their result follows certain rules. Let’s look at those rules -

Rules for constant expressions

  • A comparison operation between two untyped constants always outputs an untyped boolean constant.

    const a = 7.5 > 5        // true (untyped boolean constant)
    const b = "xyz" < "uvw"  // false (untyped boolean constant)
  • For any other operation (except shift) -

    • If both the operands are of the same type (ex - both are untyped integer constants), the result is also of the same type. For example, the expression 25/2 yields 12 not 12.5. Since both the operands are untyped integers, the result is truncated to an integer.

    • If the operands are of different type, the result is of the operand’s type that is broader as per the rule: integer < rune < floating-point < complex.

      const a = 25/2      // 12 (untyped integer constant)
      const b = (6+8i)/2  // (3+4i) (untyped complex constant) 
  • Shift operation rules are a bit complex. First of all, there are some requirements -

    • The right operand of a shift expression must either have an unsigned integer type or be an untyped constant that can represent a value of type uint.

    • The left operand must either have an integer type or be an untyped constant that can represent a value of type int.

      The rule - If the left operand of a shift expression is an untyped constant, the result is an untyped integer constant; otherwise the result is of the same type as the left operand.

      const a = 1 << 5          // 32 (untyped integer constant)
      const b = int32(1) << 4   // 16 (int32) 
      const c = 16.0 >> 2       // 4 (untyped integer constant) - 16.0 can represent a value of type `int`
      const d = 32 >> 3.0       // 4 (untyped integer constant) - 3.0 can represent a value of type `uint`
      
      const e = 10.50 << 2      // ILLEGAL (10.50 can't represent a value of type `int`)
      const f = 64 >> -2        // ILLEGAL (The right operand must be an unsigned int or an untyped constant compatible with `uint`)

Constant Expression Examples

Let’s see some examples of constant expressions -

package main
import "fmt"

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

Since both 25 and 2 are untyped integer constants, 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

Let’s see another 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)/24.5 + (5) * (3 + 2)/24.5 + (5) * (5)/24.5 + (25)/24.5 + 1216.5

You got it right? The result is wrong because the expression 25/2 is evaluated to 12.

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

Code Samples: github.com/callicoder/golang-tutorials

Footnotes