Constants are arguably present in every code file, and there are many benefits to using them.

  • Avoid magic literals, i.e. numbers, strings, etc. that appear directly in the code. It is not possible to read the code and see what it means at a glance. It also avoids the inconsistencies that can occur when using literals. When their values need to be modified, constants only need to be modified in one place, while literals need to be modified in multiple places, making it easy to miss inconsistencies.
  • Compared to variables, constants can perform compile-time optimizations.

The Go language also provides syntactic support for constants, which is largely consistent with the constants provided by other languages. But there are a few useful features of constants in Go that are worth knowing about.

Constants Base

The const keyword is used to define constants in the Golang:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

const PI float64 = 3.1415926
const MaxAge int = 150
const Greeting string = "hello world"

func main() {
  fmt.Println(PI)
  fmt.Println(MaxAge)
  fmt.Println(Greeting)
}

Multiple constant definitions can be combined together, e.g. several of the above constant definitions can be written in the following form.

1
2
3
4
5
const (
  PI       float64 = 3.1415926
  MaxAge   int     = 150
  Greeting string  = "hello world"
)

However, it is usually recommended to define constants of the same type, associated with each other, inside a group.

There is a big restriction on constants in Go: you can only define constants of basic types, i.e. boolean (bool), integer (unsigned uint/uint8/uint16/uint32/uint64/uintptr, signed int/int8/int16/int32/int64), floating-point (single-precision float32, double-precision float64), or the underlying type is one of these basic types. You cannot define constants of these types such as slices, arrays, pointers, structures, etc. For example, byte has an underlying type of uint8 and rune has an underlying type of int32, see the Go source code builtin.go:

1
2
3
// src/builtin/builtin.go
type byte = uint8
type rune = int32

Therefore, constants of class byte or rune can be defined:

1
2
const b byte = 128
const r rune = 'c'

Defining variables of other types will report an error during compilation.

1
2
3
4
5
6
7
8
9
type User struct {
  Name string
  Age  int
}

const u User = User{} // invalid const type User

var i int = 1
const p *int = &i // invalid const type *int

iota

The Go language code often uses iota for constant definitions, here are a few Go source codes.

Standard library time source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/time/time.go
type Month int

const (
  January Month = 1 + iota
  February
  March
  April
  May
  June
  July
  August
  September
  October
  November
  December
)

type Weekday int

const (
  Sunday Weekday = iota
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
)

Standard library net/http source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/net/http/server.go
type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

iota is a mechanism that facilitates the definition of our constants. In short, iota acts independently in each constant definition group (each const statement that appears separately counts as a group), and iota appears in the constant expression used to initialize the value of the constant, the value of iota is the line it is on in the constant group (starting at 0). The type and initialization expression can be omitted below a constant defined using iota, which then follows the type and initialization expression defined in the previous one. Let’s look at a few sets of examples:

1
2
3
4
5
6
7
const (
  One int = iota + 1
  Two
  Three
  Four
  Five
)

This is also the most commonly used way, iota appears in the first line and its value is whatever it is. In the constant definition group above, One is on line 0 (note that it counts from 0) and iota is 0, so One = 0 + 1 = 1. The next line Two omits the type and initialization expression, so Two follows the type int above, and the initialization expression is also iota + 1. But this is line 1 of the definition group, and the value of iota is 1, so Two = 1 + 1 = 2. The next line, Three, also omits the type and initialization expression, so Three follows Two and thus One’s type int, and the initialization expression is also iota + 1, but this is the second line of the definition, so Three = 2 + 1 = 3. And so on.

We can use iota in very complex initialization expressions:

1
2
3
4
5
6
const (
  Mask1 int = 1<<(iota+1) - 1
  Mask2
  Mask3
  Mask4
)

According to the above analysis Mask1~4 are 1, 3, 7, 15 in order.

In addition, there are odd numbers and even numbers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const (
  Odd1 = 2*iota + 1
  Odd2
  Odd3
)

const (
  Even1 = 2 * (iota + 1)
  Even2
  Even3
)

In a group, iota does not necessarily appear in row 0, but the value is whatever the row in which it appears:

1
2
3
4
5
6
7
const (
  A int = 1
  B int = 2
  C int = iota + 1
  D
  E
)

Above iota appears in row 2 (starting from 0) and the value of C is 2 + 1 = 3. D and E are 4, 5 respectively.

Be sure to note that the value of iota is equal to the number of rows it appears in the group, not the number of times it appears.

Values can be ignored by assigning to the empty identifier:

1
2
3
4
5
6
7
8
const (
  _ int = iota
  A // 1
  B // 2
  C // 3
  D // 4
  E // 5
)

With all this talk about the use of iota, why should I use iota? In other words, what are the advantages of iota? I think there are two points.

  • ease of definition, when the pattern is relatively fixed, we can just write out the first one, and the constants that follow don’t need to write out the type and initialization expressions.
  • Convenient to adjust the order, add and delete constant definitions. If we want to adjust the order after defining a set of constants, using iota’s definition, we just need to adjust the position, no need to modify the initialization formula, because it is not written. The same goes for adding and deleting. If we write out the initialization formula one by one and delete the middle one, the subsequent values will have to be adjusted.

For example, the source code in net/http:

1
2
3
4
5
6
7
8
9
type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

If we need to add a constant that indicates the state that is being closed. Now just write the name of the added state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosing // Added state
  StateClosed
)

If the initialization equation is written explicitly:

1
2
3
4
5
6
7
8
9
type ConnState int

const (
  StateNew ConnState = 0
  StateActive ConnState = 1
  StateIdle ConnState = 2
  StateHijacked ConnState = 3
  StateClosed ConnState = 4
)

This adds the need to change subsequent values. Also a lot more characters need to be typed 😊:

1
2
3
4
5
6
7
8
const (
  StateNew ConnState = 0
  StateActive ConnState = 1
  StateIdle ConnState = 2
  StateHijacked ConnState = 3
  StateClosing ConnState = 4
  StateClosed ConnState = 5
)

No type constants

The Golang has a special type of constants, namely untyped constants. That is, we do not explicitly specify the type when defining it. Such constants can store values beyond the usual range of types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
  "fmt"
  "math"
  "reflect"
)

const (
  Integer1 = 1000
  Integer2 = math.MaxUint64 + 1
  Float1   = 1.23
  Float2   = 1e100
  Float3   = 1e400
)

func main() {
  fmt.Println("integer1=", Integer1, "type", reflect.TypeOf(Integer1).Name())
  // Compilation errors
  // fmt.Println("integer2=", Integer2, "type", reflect.TypeOf(Integer2).Name())
  fmt.Println("integer2/10=", Integer2/10, "type", reflect.TypeOf(Integer2/10).Name())

  fmt.Println("float1=", Float1, "type", reflect.TypeOf(Float1).Name())
  fmt.Println("float2=", Float2, "type", reflect.TypeOf(Float2).Name())
  // Compilation errors
  // fmt.Println("float3=", Float3, "type", reflect.TypeOf(Float3).Name())
  fmt.Println("float3/float2=", Float3/Float2, "type", reflect.TypeOf(Float3/Float2).Name())
}

Although untyped constants can store values outside the range of normal types and can do arithmetic operations on each other, they still need to be converted back to normal types when they are used (assigned to variables, passed as parameters). If the value exceeds the normal type range, the compilation will report an error. Each untyped constant has a default type, int for integers and float64 for floating point numbers (with decimal points or scientific notation), so in the above example, we define Integer2 as an untyped constant with the value of uint64 maximum + 1, which is allowed. But if we output the value of Integer2 directly, it will cause a compile error because Integer2 will be converted to int type by default, and it stores more values than the range of int. On the other hand, we can do operations with Integer2, such as dividing by 10, and the resulting value is in the int range and can be output. (I’m using a 64-bit machine)

The following floating point types are similar, Float3 is out of the range of float64 representation, so it can’t be output directly. But the result of Float3/Float2 is in the range of float64 and can be used.

Output of the above program:

1
2
3
4
5
integer1= 1000 type int
integer2/10= 1844674407370955161 type int
float1= 1.23 type float64
float2= 1e+100 type float64
float3/float2= 1e+300 type float64

The output also shows that the default types of integer and floating point are int and float64, respectively.

Combining the iota and untyped constants we can define a set of storage units:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

const (
  _  = iota
  KB = 1 << (10 * iota)
  MB // 2 ^ 20
  GB // 2 ^ 30
  TB // 2 ^ 40
  PB // 2 ^ 50
  EB // 2 ^ 60
  ZB // 2 ^ 70,1180591620717411303424
  YB // 2 ^ 80
)

func main() {
  fmt.Println(YB / ZB)
  fmt.Println("1180591620717411303424 B = ", 1180591620717411303424/ZB, "ZB")
}

ZB has actually reached 1180591620717411303424, which is beyond the range of int representation, but we can still define ZB and YB and still operate on them when we use them, as long as the final value to be used is within the range of normal types.

Summary

The text introduces the knowledge of constants, and it is sufficient to remember two main points.

  • The value of iota is equal to the line in which it appears in the constant definition group (starting from 0).
  • Untyped constants can define values that exceed the stored range, but they must be used in such a way that they can be transferred back to the normal type range, otherwise an error will be reported. Using untyped constants, we can perform arithmetic operations on large numbers at compile time

Reference https://darjun.github.io/2021/05/30/youdontknowgo/const/