常量 Constants

常量是在运行时不能改变其值的量。Go中存在布尔常量, 符文常量,整数常量,浮点常量, 复数常量和字符串常量。其中符文、整数、浮点、复数常量统称为数值常量。例如:

true, false
'C'
42
0.618
"Hank"
1
2
3
4
5

常量可以不与标识符绑定,像上面的例子一样。也可以与标识符绑定,常量声明就是将常量与某个标识符进行绑定,例如:

const SecretOfTheUniverse = 42
1

使用绑定标识符常量,具有如下的作用:

  • 保证数据不被无意修改。
  • 简化一些长的常量值。
  • 集中处理,易于更新。
  • 将一定数据语义化。

常量声明 Constant declarations

常量声明是将一组常量表达式与常量标识符绑定。关键字 const 用于声明常量。语法上类似于 var 关键字,但没有短声明语法。

语法如下:

const 标识符 类型 = 常量表达式 // 声明单个常量,指定类型,指定值
const 标识符 = 常量表达式 // 声明单个未类型化常量,指定值
const 标识符1, 标识符2 = 常量表达式1, 常量表达式2 // 批量定义未类型化常量,指定值
// 支持括号的批量定义
const (
	标识符 类型 = 常量表达式 // 声明单个常量,指定类型,指定值
	标识符 = 常量表达式 // 声明单个未类型化常量,指定值
	标识符1, 标识符2 = 常量表达式1, 常量表达式2 // 批量定义未类型化常量,指定值
)
1
2
3
4
5
6
7
8
9

其中常量表达式指的是可以得到常量值的表达式,具体表现为:

  • bool, rune, integer, floating-point, imaginary 或 string 类型字面量。
  • 其他常量
  • 常量运算表达式
  • 结果为常量的类型转换
  • 一些内建函数的返回值。例如:unsafe.Sizeof, cap, len, real, imag, complex
  • 标识符 iota

常量可以是类型化(typed)或未类型化(untyped)的,与变量不同,变量一定是具有类型的。类型化在语法上表现为声明时指定了类型,而未类型化是声明时未指定类型。

演示:

const pi float64 = 3.1415926 // float64 型常量
const golden = 0.618 // 未类型化常量
const c1, c2, c3 = 42, "hank", true // 字面量,得到未类型化常量
const c4 = c2 // 其他常量
const c5 = 1 + 2 + 3 + 4 // 常量表达式
const c6 = string('H') // 类型转换
const c7 = len("Hank") // 内置函数
const (
    info = 1 << iota // iota 标识符
    debug // 1 << iota
    warning // 1 << iota
    error // 1 << iota
)
1
2
3
4
5
6
7
8
9
10
11
12
13

const () 批量定义语法中,支持延续定义的简便语法,指的是常量可以延续之前的声明规则。例如下列声明语法:

const (
	c1 = "Hank"
	c2	// 延续声明,相当于 c2 = "Hank" 
	c3	// 延续声明
)
1
2
3
4
5

c1, c2, c3 三个常量的值保持一致,因为声明语法一致。通常配合 iota 使用,也就是上面info, debug, warning, error 的例子,iota 使用方法请看 iota 章节。

未类型化常量,Untyped constant

声明常量时若未指定类型,同时常量值使用的是未类型化值,那常量也是未类型化常量。例如整数 42 我们不能确定具体是哪种整数类型,可以是 Int,也可以是 uint或其他整型;0.618 我们也不能确定是 float32 还是 float64,这些数据就是未类型化数据,未类型化常量相当于直接使用其值表达式。

演示:

const c1 int = 42 // 类型化常量
const c2 = 42 // 未类型化常量
1
2

未类型化的常量 c2 和 类型化的常量 c1,从表面看都是42,但 c1 只能作为 int 型进行处理,例如给其他 int 型数据赋值,而 c2 就打破了这个限制,可以给任意满足 42 语法的数据赋值,相当于直接使用 42, 演示:

const c1 int = 42 // 类型化常量
const c2 = 42 // 未类型化常量
var v1 int = c1 // 成功
var v2 int64 = c1 // 失败,c1 为 int 类型,不能使用为 int64 赋值。: cannot use c1 (type int) as type int64 in assignment
var v3 int = c2 // 成功,相当于 var v3 int = 42
var v4 int64 = c2 // 成功,相当于 var v4 int64 = 42
var v5 float64 = c2 // 成功,相当于 var v5 float64 = 42
1
2
3
4
5
6
7

注意 c2 可以为不同类型的 v3, v4, v5 赋值,是以为 c2 是未确定类型,在具体使用时再进行确定。

注意,没有未确定类型的变量,代码:

v6 := c2 // 相当于 v6 := 42
1

此时,v6 为 int 类型,因为在使用 c2 赋值时,利用 42 整数的默认类型,完成了推导,进而确定 v6 变量的类型。

除了整数是未类型化的常量,其他类型也是未类型化的常量,例如:

42 // untyped int, 未类型化整数
"Hank" // untyped string, 未类型化字符串
0.618 // unytped float,未类型化浮点数
1
2
3

未类型常量的意思是尽量保证可运算的数据不用进行复杂的类型转换而直接完成计算。Go 是强类型,运算时严格要求类型一致,那么我使用未类型化常量,就在一定程度上避免了类型不一致的问题。

任意精度的数值常量 Arbitrary precision

数值常量可表示任意精度的值而不会溢出。因此没有表示IEEE-754标准中的的负零(-0)、无穷大(infinity)和非数字值(not-a-number)的常量。

例如 math 包中一些数学常量的声明:

// 选取了部分
const (
	E   = 2.71828182845904523536028747135266249775724709369995957496696763 // https://oeis.org/A001113
	Pi  = 3.14159265358979323846264338327950288419716939937510582097494459 // https://oeis.org/A000796
	Phi = 1.61803398874989484820458683436563811772030917980576286213544862 // https://oeis.org/A001622
)
1
2
3
4
5
6

在测试输出时,会发现并没有得到完整的精度:

fmt.Println(math.Pi)
// 结果
3.141592653589793
1
2
3

是因为当这些高精度的无类型常量值被用于计算或其他操作时,会依据当时的语法环境得到某类型的最近似的值。本例中,fmt.Println 函数会使用该无类型数值的默认类型,就是 float64 作为最终处理的类型。

Go 如此设计的目的,就是让这些精度的数,在最后一刻才完成舍弃操作,尽量保证更高的精度。

尽管数值常量在该语言中可拥有任意精度, 但编译器可能使用其有限精度的内部表示来实现它们。即,每个实现必须:

  • 使用至少256位表示整数常量。
  • 使用至少256位表示浮点常量,包括复数常量及尾数部分; 使用至少32位表示指数符号。
  • 若无法精确表示一个整数常量,则给出一个错误。
  • 若由于溢出而无法表示一个浮点或复数常量,则给出一个错误。
  • 若由于精度限制而无法表示一个浮点或复数常量,则舍入为最近似的可表示常量。

iota 常量生成器 Iota constant generator

在声明常量时,iota 用于生成由 0 开始递增的未类型化整数,就是 0, 1, 2...。对于每个 const 语句,iota 会被初始化置零。

演示:

const (
   	c1 = iota // 0
	c2 = iota // 1
	c3 = iota // 2
)
const c4 = iota // 0
const (
    c5 = iota // 0
    c6	// 1,延续定义 c6 = iota
    c7	// 2,同上
)
1
2
3
4
5
6
7
8
9
10
11

iota 是在每个常量被声明后实现递增。而不是以iota每次出现作为递增时机的。

演示:

const (
    c1 = 42 
    c2 = "Hank" 
    c3 = iota // 2
)
1
2
3
4
5

常量 c3 的值是 2, 而不是 0。因为 iota 碰到 const 就会被初始化为 0,然后 const 中声明了常量 c1,此时 iota 递增为1,又声明了 c2,此时 iota 递增为2,在声明 c3 就是 2 了。可以记忆为与行索引是一致的。

也可以使用空白标识符_来跳过某个递增数,演示:

const (
   	c1 = iota // 0
	_	// 跳过了 1
	c3 // 2
)
1
2
3
4
5

iota 标识符也可参与表达式运算,得到规则更为复杂常量定义,演示为:

// 定义一组奇数
const (
    c1 = 2 * iota + 1	// 1
    c2	// 3
    c3 	// 5
    c4	// 7
)
1
2
3
4
5
6
7
// 定义一组开关状态(位运算)
const (
	c1 = 1 << iota // 1
	c2 // 2
	c3 // 4
	c4 // 8
)
1
2
3
4
5
6
7

注意 iota 是一个预声明标识符,是一个未类型化整型,初始值为0,声明位于 buildin.go 中:

const iota = 0 // Untyped int.
1

因此,可以参与常量表达式运算。