Go 中的常量
Rob Pike
2014 年 8 月 25 日
介绍
Go 是一种静态类型的语言, 不允许混合数字类型的操作. 您不能将 float64
添加到 int
中, 甚至不能将 int32
添加到 int
中. 但是这样写 1e6*time.Second
或 math.Exp(1)
甚至 1<<('.'+2.0)
都是是合法的. 在 Go 中常量与变量不同, 其行为与常规数字非常相似. 这篇文章解释了这是为什么以及它的含义.
由来: C
在 Go 的早期思考中, 我们谈到了 C 及其后代让您混合和匹配数字类型的方式引起的许多问题. 许多神秘的错误, 崩溃和可移植性问题是由组合了不同大小和 "符号" 的整数的表达式引起的. 尽管对于经验丰富的 C 程序员而言计算结果如下
unsigned int u = 1e9;
long signed int i = -1;
... i + u ...
可能很熟悉, 这这不够 显而易见. 结果有多大? 它的值是什么? 它是签名的还是未签名的?
万恶的 bug 潜伏在这.
C 有一组称为 "通用算术转换" 的规则, 它表明了它们多年来的微妙变化 (追溯地引入了更多错误).
在设计 Go 时, 我们决定通过强制数字类型不混合来避免出现雷区. 如果要添加 i
和 u
, 则必须明确说明想要的结果是什么. 给定如下
var u uint
var i int
您可以编写 uint(i)+u
或 i+int(u)
, 并清楚地表达加法的含义和类型, 但是与 C 语言不同, 您不能编写 i+u
. 您甚至不能混合使用 int
和 int32
, 即使 int
是32位类型.
这种严格性消除了错误和其他故障的常见原因. 这是 Go 的重要属性. 但这是有代价的: 有时它要求程序员使用笨拙的数字转换来修饰代码, 以清楚地表达其含义.
那么常量呢? 鉴于以上声明, 写 i
=
0
或 u
=
0
? 这里 0
的 类型 是什么? 在诸如 i
=
int(0)
之类的简单上下文中要求常量进行类型转换将是不合理的.
我们很快意识到答案在于使数字常量的工作方式不同于它们在其他类似 C 的语言中的行为方式. 经过大量的思考和实验, 我们提出了一种我们认为感觉几乎总是对的的设计, 使程序员无需一直转换常量, 而能够编写类似 math.Sqrt(2)
之类的东西. 由编译器编译.
简而言之, 在大多数情况下, Go 中的常量都可以工作. 让我们看看这是怎么发生的.
术语
首先, 快速定义. 在 Go 中, const
是一个关键字, 它为标量值引入了名称, 例如 2
或 3.14159
或 "spunptious"
. 这样的值在 Go 中称为 常量. 常量也可以通过根据常量构建的表达式来创建, 例如 2+3
或 2+3i
或 math.Pi/2
或 ("go"+" pher")
.
某些语言没有常量, 而另一些语言则具有常量的更一般定义或 const
一词的应用. 例如, 在 C 和 C++ 中, const
是一种类型限定符, 可以将更复杂的值编码为更复杂的属性.
但是在 Go 中, 常数只是一个简单且不变的值, 从这里开始, 我们仅在谈论 Go 中的常量.
字符常量
数字常量有很多种, 整数, 浮点数, 符文, 有符号, 无符号, 虚数, 复数 - 因此我们从更简单的常量形式开始: 字符串. 字符串常量易于理解, 并提供了一个较小的空间来探索 Go 中常量的类型问题.
字符串常量在双引号之间包含一些文本. (Go 也有原始字符串文字, 用反引号 ```` 括起来, 但出于讨论目的, 它们具有相同的属性.) 这是一个字符串常量:
"Hello, World"
(有关字符串表示和解释的更多详细信息, 请参阅 此博客文章.)
此字符串常量有什么类型? 显而易见的答案是 string
, 但这 是错误的.
这是一个 无类型的字符串常量, 也就是说, 它是一个尚不具有固定类型的常量文本值. 是的, 它是一个字符串, 但不是 string
类型的 Go 值. 即使给定名称, 它仍然是未类型化的字符串常量:
const hello = "Hello, World"
声明后, hello
也是一个无类型的字符串常量. 一个没有类型的常量只是一个值, 尚未给定类型的常量将迫使它遵守严格的规则, 以防止合并不同类型的值.
正是 无类型 常量的概念使我们可以在 Go 中自由使用常量.
那么带 类型 字符串常量是什么? 这是给定的一种类型, 例如:
const typedHello string = "Hello, World"
请注意, typedHello
的声明在等号之前具有显式的 string
类型. 这意味着 typedHello
具有 Go 类型 string
, 并且不能分配给其他类型的 Go 变量. 也就是说, 如下代码是有效的:
var s string
s = typedHello
fmt.Println(s)
但下面这样的不对:
type MyString string
var m MyString
m = typedHello // 类型错误
fmt.Println(m)
变量 m
的类型为 MyString
, 并且不能为其分配其他类型的值. 只能为其分配类型为 MyString
的值, 如下所示:
const myStringHello MyString = "Hello, World"
m = myStringHello // 这样是可以的
fmt.Println(m)
或通过强制转换来解决问题, 例如:
m = MyString(typedHello)
fmt.Println(m)
返回给我们的 untyped 字符串常量, 它具有有用的属性, 因为它没有类型, 因此将其分配给类型变量不会导致类型错误. 也就是说, 我们可以这样写
m = "Hello, World"
或者这样写
m = hello
因为不同于类型常量 typedHello
和 myStringHello
, 无类型常量 "Hello, 世界"
和 hello
没有类型. 将它们分配给与字符串兼容的任何类型的变量都可以正常工作.
这些无类型的字符串常量当然是字符串, 因此它们只能在允许使用字符串的地方使用, 但它们是没有 类型 的 string
.
默认类型
作为一名 Go 程序员, 您肯定已经看过很多声明, 例如
str := "Hello, World"
现在您可能会问:"如果未定义类型的常量, str
如何在此变量声明中获取类型?" 答案是无类型的常量具有默认类型, 如果需要其类型 (不提供任何类型), 则它将隐式类型转换为值. 对于无类型的字符串常量, 该默认类型显然是 string
, 因此
str := "Hello, World"
或者
var str = "Hello, World"
的意思与
var str string = "Hello, World"
都是一样样滴.
思考无类型常量的一种方法是, 它们生活在一种理想的值空间中, 该空间的约束性比 Go 的完整类型系统要小. 但是要对它们执行任何操作, 我们需要将它们分配给变量, 并且在这种情况下 variable (不是常量本身) 需要一个类型, 并且常量可以告诉变量应该具有什么类型. 在此示例中, str
成为类型为 string
的值, 因为未类型化的字符串常量为其声明提供了默认类型 string
.
在这样的声明中, 使用类型和初始值声明变量. 但是, 有时当我们使用常量时, 值的目的地并不十分清楚. 例如, 考虑以下语句:
fmt.Printf("%s", "Hello, World")
fmt.Printf
函数的签名是
func Printf(format string, a ...interface{}) (n int, err error)
也就是说它的参数 (在格式字符串之后) 是接口值. 当使用无类型常量调用 fmt.Printf
时会发生以下情况: 创建接口值以作为参数传递, 并且为该参数存储的具体类型是常量的默认类型. 此过程类似于我们之前使用无类型的字符串常量声明初始化值时看到的过程.
您可以在本示例中看到结果, 该示例使用格式 %v
打印值, 并使用 %T
打印要传递给 fmt.Printf
函数的值的类型:
fmt.Printf("%T: %v.", "Hello, World", "Hello, World")
fmt.Printf("%T: %v.", hello, hello)
如果常量具备类型, 它将进入接口, 如下所示:
fmt.Printf("%T: %v.", myStringHello, myStringHello)
(有关接口值如何工作的更多信息, 可参阅 此博客文章 的第一部分.)
总而言之, 类型化常量遵循 Go 中类型化值的所有规则. 另一方面, 未类型化的常量不会以相同的方式携带 Go 类型, 并且可以更自由地混合和匹配. 但是, 它确实具有默认类型, 该默认类型仅在没有其他类型信息可用时才公开.
通过语法确定默认类型
无类型常量的默认类型也可以通过语法来确定. 对于字符串常量, 唯一可能的隐式类型是 string
. 对于 数字常量, 隐式类型的种类更多. 整数常量默认为 int
, 浮点常量 float64
, 符文常量为 rune
(int32
的别名) 以及虚数常量到 complex128
. 这是我们的规范打印语句, 用于重复显示默认类型:
fmt.Printf("%T %v.", 0, 0)
fmt.Printf("%T %v.", 0.0, 0.0)
fmt.Printf("%T %v.", 'x', 'x')
fmt.Printf("%T %v.", 0i, 0i)
(练习: 解释 'x'
的结果.)
布尔值
我们所说的有关无类型的字符串常量的所有内容都可以用无类型的布尔常量表示. 值 true
和 false
是无类型的布尔常量, 可以分配给任何布尔变量, 但是一旦指定类型, 就不能将布尔变量混合使用:
type MyBool bool
const True = true
const TypedTrue bool = true
var mb MyBool
mb = true // 这样可以
mb = True // 这样也可以
mb = TypedTrue // 这样就不行了
fmt.Println(mb)
运行示例, 看看会发生什么, 然后注释掉 "Bad" 行并再次运行. 这里的模式与字符串常量完全相同.
浮点数
在大多数情况下浮点常量就像布尔常量一样. 我们的标准示例在转换中按预期工作:
type MyFloat64 float64
const Zero = 0.0
const TypedZero float64 = 0.0
var mf MyFloat64
mf = 0.0 // 这样可以
mf = Zero // 这样也可以
mf = TypedZero // 这样就不行了
fmt.Println(mf)
一种情况是 Go 中有 两种 浮点类型: float32
和 float64
. 浮点常量的默认类型为 float64
, 尽管可以将一个无类型的浮点常量分配给 float32
值就可以了:
var f32 float32
f32 = 0.0
f32 = Zero // 这样是可以的: Zero 是无类型的
f32 = TypedZero // 这就不行了: TypedZero 类型是 float64 而不是 float32.
fmt.Println(f32)
浮点值是介绍溢出概念或值范围的好地方.
数字常数存在于任意精度的数字空间中. 它们只是常规数字. 但是, 当将它们分配给变量时, 该值必须能够适合目标. 我们可以声明一个非常大的常量:
const Huge = 1e1000
毕竟这只是一个数字, 但我们无法分配它, 甚至无法打印出来. 该语句甚至不会编译:
fmt.Println(Huge)
这里的错误是 "constant 1.00000e+1000 overflows float64", 错误是正确的. 但是 Huge
可能很有用: 如果结果可以在 float64
范围内表示, 我们可以在具有其他常量的表达式中使用它并使用这些表达式的值. 声明如下,
fmt.Println(Huge / 1e999)
不出意外的话, 会打印 10
.
类似的情况, 浮点常量可能具有非常高的精度, 因此涉及它们的算法会更加准确. 给 math 包中定义的常量提供的数字比 float64
中可用的数字多得多. 这是 math.Pi
的定义:
Pi = 3.14159265358979323846264338327950288419716939937510582097494459
当该值分配给变量时, 某些精度将丢失. 分配将创建最接近于高精度值的 float64
(或 float32
)值. 来看看下面这个代码片段
pi := math.Pi
fmt.Println(pi)
会打印输出 3.141592653589793
.
可用的位数如此之多意味着 Pi/2
之类的计算或其他更复杂的求值方法在赋予结果之前可以具有更高的精度, 从而使涉及常数的计算更容易编写而不会损失精度. 这也意味着在任何情况下都不会在常量表达式中出现无限点, 软下溢和 NaNs
这样的浮点转角情况. (除以常数零就是编译时错误, 当所有内容都是数字时, 就不会出现 "不是数字" 之类的东西.)
复数
复数常量的行为很像浮点常量. 这是我们现在熟悉的连字转换成复数的版本:
type MyComplex128 complex128
const I = (0.0 + 1.0i)
const TypedI complex128 = (0.0 + 1.0i)
var mc MyComplex128
mc = (0.0 + 1.0i) // 这样可以
mc = I // 这样也可以
mc = TypedI // 这样就不行了
fmt.Println(mc)
复数的默认类型为 complex128
, 它是由两个 float64
值组成的较大精度版本.
为了使示例清晰易懂, 我们写出了完整的表达式 (0.0+1.0i)
, 但是此值可以缩短为 0.0+1.0i
, 1.0i
甚至是 1i
.
这里玩个把戏. 我们知道, 在 Go 中数字常数只是一个数字. 如果该数字是没有虚部的复数, 即实数, 该怎么办? 代码如下:
const Two = 2.0 + 0i
这是一个无类型的复数常量. 即使没有虚部, 表达式的 语法 仍将其定义为默认类型 complex128
. 因此, 如果使用它声明变量, 则默认类型为 complex128
. 代码片段如下
s := Two
fmt.Printf("%T: %v.", s, s)
会打印输出 complex128
(2+0i)
. 但是从数字上讲, 可以将 Two
存储在标量浮点数中, 即 float64
或 float32
, 而不会丢失信息. 因此, 我们可以在初始化或分配中将 Two
分配给 float64
, 而不会出现问题:
var f float64
var g float64 = Two
f = Two
fmt.Println(f, "and", g)
输出为 2
和 2
. 即使 Two
是一个复数常量, 也可以将其分配给标量浮点变量. 这样的常数可以 "交叉" 类型的能力将被证明是有用的.
整数
最后我们来谈谈整数. 它们具有更多的活动部分 - 许多大小, 有符号或无符号, 以及更多 - 但它们遵循相同的规则. 上一次, 这是我们熟悉的示例, 这次仅使用 int
:
type MyInt int
const Three = 3
const TypedThree int = 3
var mi MyInt
mi = 3 // 这样可以
mi = Three // 这样也可以
mi = TypedThree // 这样就不行了
fmt.Println(mi)
可以试试给以下任何整数类型构建相同的示例:
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr
(加上 uint8
的别名 byte
和 int32
的别名 rune
). 可以很多, 但是到现在, 常量工作方式的模式应该已经足够熟悉, 您可以看到事情将如何发展.
如上所述, 整数有两种形式, 每种形式都有其自己的默认类型: int
用于诸如 123
或 0xFF
或 -14
以及 rune
用于诸如 'a', '世' 或 '.' 的引号字符.
无常量形式的默认类型为无符号整数类型. 但是, 无类型常量的灵活性意味着只要我们清楚类型, 就可以使用简单常量初始化无符号整数变量. 这类似于我们如何使用虚数为零的复数来初始化 float64
的方法. 这是初始化 uint
的几种不同方法; 所有都是等效的, 但是所有都必须明确提及类型, 以使结果不带符号.
var u uint = 17
var u = uint(17)
u := uint(17)
与浮点值部分中提到的范围问题类似, 并非所有整数值都可以适合所有整数类型. 可能会出现两个问题: 该值可能太大, 或者可能是分配给无符号整数类型的负值. 例如, int8
的范围是 -128 到 127, 因此永远不能将超出该范围的常量分配给 int8
类型的变量:
var i8 int8 = 128 // 错误: too large.
类似地, uint8
也称为 byte
, 范围为 0 到 255, 因此无法将大或负常量分配给 uint8
:
var u8 uint8 = -1 // 错误: negative value.
这种类型检查可以发现如下错误:
type Char byte
var c Char = '世' // 错误: '世' has value 0x4e16, too large.
如果编译器抱怨您使用常量, 则可能是真正的错误.
练习: 最大的 unsigned int
这是一个有益的小练习. 我们如何表达 uint
的最大值的常数? 如果我们在谈论 uint32
而不是 uint
, 我们可以这样写
const MaxUint32 = 1<<32 - 1
但我们想要 uint
, 而不是 uint32
. int
和 uint
类型具有相同的未指定位数, 可以是 32 或 64. 由于可用位数取决于体系结构, 我们不能只写下一个值.
Go 的整数被定义为使用的 two-s-complement 算法 的迷们知道 -1
的表示形式位设置为 1, 因此 -1
的位模式在内部与最大无符号整数的位模式相同. 因此, 我们可能认为我们可以写
const MaxUint uint = -1 // 错误: negative value
但这是非法的, 因为 -1 不能用无符号变量表示; -1
不在无符号值的范围内. 出于同样的原因, 转换也无济于事:
const MaxUint uint = uint(-1) // 错误: negative value
即使在运行时可以将 -1 的值转换为无符号整数, 但常量 conversions 的规则在编译时也禁止这种强制性. 也就是说, 下面的代码是有效:
var u uint
var v = -1
u = uint(v)
但这仅仅是因为 v
是一个变量; 如果我们将 v
设为一个常数, 甚至是一个无类型的常数, 我们都会回到禁区:
var u uint
const v = -1
u = uint(v) // 错误: negative value
我们返回之前的方法, 但尝试使用 ^0
代替 -1
, 即任意数目的零位的按位求反. 但这也失败了, 原因类似: 在数值空间中, ^0
表示无限个, 因此如果将其分配给任何固定大小的整数, 我们将丢失信息:
const MaxUint uint = ^0 // 错误: overflow
那么我们如何将最大的无符号整数表示为常数呢?
关键是将操作限制为 uint
中的位数, 并避免在 uint
中无法表示的值 (例如负数). 最简单的 uint
值是类型化的常量 uint(0)
. 如果 uints
具有 32 或 64 位, 则 uint(0)
具有 32 或 64 个零位. 如果我们将这些位中的每一个取反, 我们将获得正确的一位数, 这是最大的 uint
值.
因此, 我们不会转换未类型化常量 0
的位, 而是会翻转类型化常量 uint(0)
的位. 那么, 这就是我们的常数:
const MaxUint = ^uint(0)
fmt.Printf("%x.", MaxUint)
在当前执行环境中, 表示 uint
所需的位数 (在 演练场上, 它是32) 是多少, 此常数正确表示 uint
类型的变量可以容纳的最大值.
如果您了解使我们获得此结果的分析, 那么您将了解有关 Go 中常量的所有重要信息.
数字
Go 中无类型常量的概念意味着所有数字常量, 无论是整数, 浮点数, 复数, 甚至是字符值, 都生活在一种统一的空间中. 当我们将它们带入变量, 赋值和运算的计算世界时, 实际类型才有意义. 但是只要我们停留在数字常数的世界中, 我们就可以随意混合和匹配值. 所有这些常量的数值为 1:
1
1.000
1e3-99.0*10-9
'.01'
'.0001'
'b' - 'a
1.0+3i-3.0i
因此, 尽管它们具有不同的隐式默认类型, 但以无类型常量的形式编写, 但可以将它们分配给任何整数类型的变量:
var f float32 = 1
var i int = 1.000
var u uint32 = 1e3 - 99.0*10.0 - 9
var c float64 = '.01'
var p uintptr = '.0001'
var r complex64 = 'b' - 'a'
var b byte = 1.0 + 3i - 3.0i
fmt.Println(f, i, u, c, p, r, b)
该代码段的输出为 1 1 1 1 1 (1+0i) 1
.
您甚至可以这样玩
var f = 'a' * 1.5
fmt.Println(f)
结果是 145.5, 除了证明一点之外, 没什么实际意义.
但是, 这些规则的真正意义在于灵活性. 这种灵活性意味着, 尽管在 Go 中在同一表达式中混合使用浮点数和整数变量甚至 int
和 int32
变量都是非法的, 但对于写
sqrt2 := math.Sqrt(2)
或者是
const millisecond = time.Second/1e3
或者是
bigBufferWithHeader := make([]byte, 512+1e6)
都可以获得符合您期望的结果.
因为在 Go 中, 数字常量可以按您期望的那样工作: 就像数字一样.
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: