1

Introduction

Constants can be said to exist in every code file. There are many advantages to using constants:

  • Avoid magic literals, that is, numbers, strings, etc. that appear directly in the code. When you read the code, you can't see its meaning at a glance. In addition, it can avoid the inconsistency that may occur when using literals. When their values need to be modified, the constants only need to be modified in one place, and the literals need to be modified in many places, which is easy to miss and cause inconsistencies;
  • Relative to variables, constants can perform compile-time optimization.

Go language also provides constant syntax support, which is basically the same as the constants provided by other languages. But there are several useful features of constants in Go that are worth knowing about.

Constant basis

const keyword to define constants in the Go language:

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. For example, several constant definitions above can be written in the following form:

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

However, it is generally recommended to define related constants of the same type in a group.

Constants in the Go language have a big limitation: only basic types of constants can be defined, namely Boolean types ( bool ), integers (unsigned uint/uint8/uint16/uint32/uint64/uintptr , signed int/int8/int16/int32/int64 ), floating-point numbers (single-precision float32 , double-precision float64 ), or The underlying types are the types of these basic types. You cannot define constants of these types such as slices, arrays, pointers, and structures. For example, byte underlying type uint8 , rune underlying type int32 , see Go source builtin.go :

// src/builtin/builtin.go
type byte = uint8
type rune = int32

Therefore, the class can be defined as a constant byte or rune

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

Defining other types of variables will report an error at compile time:

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

Constant definitions in Go language code often use iota . Let's look at some Go source codes.

Standard library time source code:

// 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:

// src/net/http/server.go
type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

iota is a mechanism for us to define constants. Simply put, iota independently in each constant definition group (each const statement that appears separately is counted as a group), iota appears in the constant expression used to initialize the constant value, and the value of iota The first few lines in the constant group (starting from 0) . iota . In this case, the type and initialization expression of the previous definition will be used. Let's look at a few sets of examples:

const (
  One int = iota + 1
  Two
  Three
  Four
  Five
)

This is also the most commonly used method, where iota appears in the first row, and what is its value. In the above constant definition group, One in line 0 (note that the counting starts from 0), iota is 0, so One = 0 + 1 = 1 .
The next line Two omits the type and initialization expression, so Two uses the above type int , and the initialization expression is also iota + 1 . But this is the first row in the definition group, and iota is 1, so Two = 1 + 1 = 2 .
The next line Three also omits the type and initialization expression, so Three follows Two and then the One type int . The initialization expression is also iota + 1 , but this is the second line of the definition, so Three = 2 + 1 = 3 . And so on.

iota in very complex initialization expressions:

const (
  Mask1 int = 1<<(iota+1) - 1
  Mask2
  Mask3
  Mask4
)

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

There are also odd and even numbers:

const (
  Odd1 = 2*iota + 1
  Odd2
  Odd3
)

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

In a group, iota does not necessarily appear in the 0th row, but it appears in the first few rows, the value is :

const (
  A int = 1
  B int = 2
  C int = iota + 1
  D
  E
)

The above iota appears in the second row (starting with 0), C value 2 + 1 = 3 . D and E are 4 and 5, respectively.

must be noted that iota is equal to the row where it appears in the group, not the number of times it appears.

You can ignore the value by assigning to the empty identifier:

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

Having said so many iota , why use iota ? In other words, what are the advantages of iota I think there are two points:

  • It is convenient to define. When the pattern is relatively fixed, we can only write the first one, and the type and initialization expression of the subsequent constants do not need to be written;
  • It is convenient to adjust the order, add and delete constant definitions. If we want to adjust the order after defining a set of constants, use iota , only need to adjust the position, no need to modify the initialization formula, because there is no writing. Adding and deleting are also the same. If we write out the initialization formulas one by one and delete the middle one, the subsequent values must be adjusted.

For example, the source code in net/http

type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

If we need to add a constant, it means the state is being closed. Now only need to write the new state name:

type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosing // 新增的状态
  StateClosed
)

If it is explicitly written out the initialization formula:

type ConnState int

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

At this time, the new value needs to be changed. In addition, there are a lot more characters to type 😊:

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

Untyped constant

There is a special kind of constant in the Go language, that is, an untyped constant. That is, when defining, we do not explicitly specify the type. Such constants can store values beyond the range of conventional types:

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())
  // 编译错误
  // 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())
  // 编译错误
  // 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 beyond the range of normal types and can perform arithmetic operations with each other, they still need to be converted back to normal types when they are used (assigned to variables and passed as parameters). If the value exceeds the range of the normal type, the compilation will report an error. Every untyped constant has a default type. The default type for integers is int , and the default type for floating-point numbers (with decimal points or scientific notation is regarded as floating-point numbers) is float64 . So in the above example, we define Integer2 as an untyped constant, the value is uint64 + 1, which is allowed. But if we directly output Integer2 , it will cause a compilation error, because Integer2 will be converted to int default, and its stored value exceeds the range of int On the other hand, we can use Integer2 for calculations, such as dividing by 10, the value obtained is in the int , which can be output. (I am using a 64-bit machine)

The following floating-point number types are similar. Float3 exceeds the float64 , so it cannot be output directly. However Float3/Float2 result in float64 range, may be used.

The output of the above program:

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

It can also be seen from the output that the default types of integer and floating point are int and float64 respectively.

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

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 actually reached 1180591620717411303424, which exceeds the int , but we can still define ZB and YB , and also perform operations on them during use, as long as the final value to be used is within the range of the normal type.

to sum up

This article introduces the knowledge of constants, just remember two points:

  • iota is equal to the number of lines in the constant definition group (starting from 0);
  • Untyped constants can define values that exceed the storage range, but they must be able to return to the range of normal types when used, otherwise an error will be reported.

Using untyped constants, we can perform arithmetic operations on large numbers at compile time.

reference

  1. "Go Programming Language"
  2. Go GitHub you don't know: https://github.com/darjun/you-dont-know-go

I

My blog: https://darjun.github.io

Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~


darjun
2.9k 声望358 粉丝