Go 博客
常量
引言
Go 是一种静态类型语言,不允许混合数值类型的操作。您不能将 float64
添加到 int
中,甚至不能将 int32
添加到 int
中。然而,编写 1e6*time.Second
或 math.Exp(1)
甚至 1<<(' '+2.0)
却是合法的。在 Go 中,常量与变量不同,它们的行为非常像普通数字。本文解释了这是为什么以及这意味着什么。
背景:C 语言
在早期考虑 Go 的时候,我们讨论了 C 语言及其派生语言允许混合和匹配数值类型所导致的一些问题。许多神秘的 bug、崩溃和可移植性问题都是由组合不同大小和“符号性”的整数的表达式引起的。尽管对于经验丰富的 C 程序员来说,像这样计算的结果
unsigned int u = 1e9;
long signed int i = -1;
... i + u ...
可能很熟悉,但它并非先验地显而易见。结果有多大?它的值是多少?它是带符号的还是无符号的?
恶劣的 bug 潜伏于此。
C 语言有一套称为“常用算术转换”的规则,它们多年来的变化(追溯地引入了更多的 bug)表明了它们的微妙之处。
在设计 Go 时,我们决定通过强制规定数值类型之间不允许混合来避免这个雷区。如果您想添加 i
和 u
,则必须明确说明您想要的结果是什么。假设有
var u uint
var i int
您可以编写 uint(i)+u
或 i+int(u)
,这两种写法都清楚地表达了加法的含义和类型,但与 C 不同,您不能编写 i+u
。您甚至不能混合使用 int
和 int32
,即使在 int
是 32 位类型的情况下也不行。
这种严格性消除了 bug 和其他故障的常见原因。它是 Go 的一个重要特性。但它也有代价:有时需要程序员用笨拙的数值转换来装饰他们的代码,以清晰地表达其含义。
那么常量呢?考虑到上面的声明,是什么使得编写 i
=
0
或 u
=
0
合法呢? 0
的类型是什么?要求常量在诸如 i
=
int(0)
这样的简单上下文中进行类型转换是不合理的。
我们很快意识到,答案在于让数值常量的行为不同于它们在其他类似 C 的语言中的行为。经过大量的思考和实验,我们提出了一种我们认为几乎总是正确的方案,它使得程序员无需一直转换常量,同时又能够编写诸如 math.Sqrt(2)
这样的代码而不会被编译器责备。
简而言之,Go 中的常量几乎总是能够正常工作。让我们看看这是如何实现的。
术语
首先,一个简短的定义。在 Go 中,const
是一个关键字,用于引入标量值的名称,例如 2
或 3.14159
或 "scrumptious"
。这些值,无论是否命名,在 Go 中都称为常量。常量也可以通过由常量构建的表达式创建,例如 2+3
或 2+3i
或 math.Pi/2
或 ("go"+"pher")
。
有些语言没有常量,有些语言对常量或单词 const
的应用有更一般的定义。例如,在 C 和 C++ 中,const
是一个类型限定符,可以编码更复杂值的更复杂的属性。
但在 Go 中,常量只是一个简单、不变的值,从这里开始,我们只讨论 Go。
字符串常量
有多种数值常量——整数、浮点数、rune、有符号、无符号、虚数、复数——所以让我们从一种更简单的常量形式开始:字符串。字符串常量易于理解,并提供了一个更小的范围来探讨 Go 中常量的类型问题。
字符串常量用双引号将一些文本括起来。(Go 也有原始字符串字面量,用反引号 ``
括起来,但出于本讨论的目的,它们具有所有相同的属性。)这是一个字符串常量
"Hello, 世界"
(有关字符串表示和解释的更多详细信息,请参阅这篇博客文章。)
这个字符串常量的类型是什么?显而易见的答案是 string
,但这不对。
这是一个无类型字符串常量,也就是说,它是一个尚未具有固定类型的常量文本值。是的,它是一个字符串,但它不是类型为 string
的 Go 值。即使给它一个名称,它仍然是一个无类型字符串常量
const hello = "Hello, 世界"
在此声明之后,hello
也是一个无类型字符串常量。无类型常量只是一个值,尚未赋予一个定义的类型,从而强制它遵守防止组合不同类型值的严格规则。
正是这种无类型常量的概念,使得我们在 Go 中能够非常自由地使用常量。
那么,什么是有类型字符串常量呢?它是被赋予了类型的常量,像这样
const typedHello string = "Hello, 世界"
注意,typedHello
的声明在等号前面有一个明确的 string
类型。这意味着 typedHello
具有 Go 类型 string
,并且不能赋值给不同类型的 Go 变量。也就是说,这段代码可以工作
var s string s = typedHello fmt.Println(s)
但这段代码不行
type MyString string var m MyString m = typedHello // Type error fmt.Println(m)
变量 m
的类型是 MyString
,不能被赋给不同类型的值。它只能被赋给类型为 MyString
的值,像这样
const myStringHello MyString = "Hello, 世界" m = myStringHello // OK fmt.Println(m)
或者通过强制进行类型转换,像这样
m = MyString(typedHello) fmt.Println(m)
回到我们的无类型字符串常量,它有一个很有用的特性:由于它没有类型,将其赋值给有类型变量不会导致类型错误。也就是说,我们可以写
m = "Hello, 世界"
或者
m = hello
因为,与有类型的常量 typedHello
和 myStringHello
不同,无类型的常量 "Hello, 世界"
和 hello
没有类型。将它们赋值给任何与字符串兼容的类型的变量都可以正常工作,不会出错。
这些无类型字符串常量当然是字符串,所以它们只能在允许使用字符串的地方使用,但它们不具有 string
类型。
默认类型
作为一名 Go 程序员,您肯定见过许多像这样的声明
str := "Hello, 世界"
现在您可能会问:“如果常量是无类型的,那么在这个变量声明中,str
如何获得类型?”答案是,无类型常量有一个默认类型,如果需要类型但未提供,它会将这个隐式类型传递给值。对于无类型字符串常量,这个默认类型显然是 string
,所以
str := "Hello, 世界"
或者
var str = "Hello, 世界"
意思是完全相同的
var str string = "Hello, 世界"
一种思考无类型常量的方式是,它们存在于一种理想的值空间中,这种空间比 Go 的完整类型系统限制更少。但要用它们做任何事情,我们需要将它们赋值给变量,当发生这种情况时,变量(而不是常量本身)需要一个类型,而常量可以告诉变量它应该具有什么类型。在这个例子中,str
成为一个类型为 string
的值,因为无类型字符串常量将其默认类型 string
赋予了声明。
在这样的声明中,变量被声明时指定了类型和初始值。然而,有时当我们使用常量时,值的目的地并不那么明确。例如,考虑这个语句
fmt.Printf("%s", "Hello, 世界")
fmt.Printf
的函数签名是
func Printf(format string, a ...interface{}) (n int, err error)
也就是说,它的参数(格式字符串之后)是接口值。当用一个无类型常量调用 fmt.Printf
时,会创建一个接口值作为参数传递,并且为该参数存储的具体类型是常量的默认类型。这个过程类似于我们前面在使用无类型字符串常量声明初始值时所看到的。
您可以在这个例子中看到结果,它使用格式 %v
来打印值,使用 %T
来打印传递给 fmt.Printf
的值的类型
fmt.Printf("%T: %v\n", "Hello, 世界", "Hello, 世界") fmt.Printf("%T: %v\n", hello, hello)
如果常量有类型,那么该类型会进入接口,如下例所示
fmt.Printf("%T: %v\n", myStringHello, myStringHello)
(有关接口值如何工作的更多信息,请参阅这篇博客文章的前几节。)
总而言之,有类型常量遵守 Go 中有类型值的所有规则。另一方面,无类型常量不以相同的方式携带 Go 类型,可以更自由地混合和匹配。然而,它确实有一个默认类型,该类型仅在没有其他类型信息可用时才暴露出来。
由语法决定的默认类型
无类型常量的默认类型由其语法决定。对于字符串常量,唯一可能的隐式类型是 string
。对于数值常量,隐式类型有更多种类。整数常量默认类型为 int
,浮点常量默认为 float64
,rune 常量默认为 rune
(int32
的别名),虚数常量默认为 complex128
。这是我们反复使用的标准打印语句,以展示默认类型的实际效果
fmt.Printf("%T %v\n", 0, 0) fmt.Printf("%T %v\n", 0.0, 0.0) fmt.Printf("%T %v\n", 'x', 'x') fmt.Printf("%T %v\n", 0i, 0i)
(练习:解释 'x'
的结果。)
布尔值
我们关于无类型字符串常量所说的一切也适用于无类型布尔常量。值 true
和 false
是无类型布尔常量,可以赋值给任何布尔变量,但一旦给定类型,布尔变量就不能混合使用
type MyBool bool const True = true const TypedTrue bool = true var mb MyBool mb = true // OK mb = True // OK mb = TypedTrue // Bad fmt.Println(mb)
运行这个例子看看会发生什么,然后注释掉“Bad”那一行再运行一次。这里的模式与字符串常量完全一致。
浮点数
浮点常量在大多数方面都像布尔常量一样。我们的标准例子在翻译中按预期工作
type MyFloat64 float64 const Zero = 0.0 const TypedZero float64 = 0.0 var mf MyFloat64 mf = 0.0 // OK mf = Zero // OK mf = TypedZero // Bad fmt.Println(mf)
一个小问题是 Go 中有两种浮点类型:float32
和 float64
。浮点常量的默认类型是 float64
,尽管无类型浮点常量也可以很好地赋值给 float32
值
var f32 float32 f32 = 0.0 f32 = Zero // OK: Zero is untyped f32 = TypedZero // Bad: TypedZero is float64 not float32. fmt.Println(f32)
浮点值是引入溢出或值范围概念的好地方。
数值常量存在于任意精度的数值空间中;它们就是普通数字。但是当它们被赋值给变量时,值必须能够放入目标中。我们可以声明一个非常大的值的常量
const Huge = 1e1000
——毕竟它只是一个数字——但我们不能赋值它,甚至不能打印它。这条语句甚至无法编译
fmt.Println(Huge)
错误是“常量 1.00000e+1000 溢出 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
或其他更复杂的计算可以保留更多精度,直到结果被赋值,这使得涉及常量的计算更容易编写而不会丢失精度。这也意味着在常量表达式中不会出现诸如无穷大、软下溢和 NaN
等浮点数边界情况。(除以常量零是编译时错误,而且当一切都是数字时,就不存在“不是数字”这样的东西。)
复数
复数常量的行为很像浮点常量。以下是我们将熟悉的例子翻译成复数版本
type MyComplex128 complex128 const I = (0.0 + 1.0i) const TypedI complex128 = (0.0 + 1.0i) var mc MyComplex128 mc = (0.0 + 1.0i) // OK mc = I // OK mc = TypedI // Bad 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\n", 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
and
2
。尽管 Two
是一个复数常量,但它可以赋值给标量浮点变量。常量这种“跨越”类型的能力将被证明很有用。
整数
最后,我们来看整数。它们有更多的变化部分——多种大小、有符号或无符号,等等——但它们遵循相同的规则。这是我们熟悉的例子,最后一次,这次只使用 int
type MyInt int const Three = 3 const TypedThree int = 3 var mi MyInt mi = 3 // OK mi = Three // OK mi = TypedThree // Bad fmt.Println(mi)
同样的例子可以为任何整数类型构建,这些类型是
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr
(加上 byte
是 uint8
的别名,rune
是 int32
的别名)。种类很多,但常量的工作模式应该足够熟悉了,您可以看出它们将如何表现。
如上所述,整数有几种形式,每种形式都有自己的默认类型:对于像 123
或 0xFF
或 -14
这样的简单常量是 int
,对于像 ‘a’、‘世’ 或 ‘\r’ 这样的带引号字符是 rune
。
没有常量的默认类型是无符号整数类型。然而,无类型常量的灵活性意味着只要我们明确指定类型,就可以使用简单常量初始化无符号整数变量。这类似于我们如何使用虚部为零的复数初始化 float64
。这里有几种不同的初始化 uint
的方法;它们都是等价的,但所有方法都必须显式提及类型,结果才能是无符号的。
var u uint = 17
var u = uint(17)
u := uint(17)
与浮点值部分中提到的范围问题类似,并非所有整数值都能适应所有整数类型。可能会出现两个问题:值可能太大,或者值是负数并被赋给无符号整数类型。例如,int8
的范围是 -128 到 127,因此超出该范围的常量永远不能赋值给类型为 int8
的变量
var i8 int8 = 128 // Error: too large.
类似地,uint8
,也称为 byte
,范围是 0 到 255,因此较大的或负的常量不能赋值给 uint8
var u8 uint8 = -1 // Error: negative value.
这种类型检查可以捕获像这样的错误
type Char byte var c Char = '世' // Error: '世' has value 0x4e16, too large.
如果编译器对您使用常量的方式发出警告,那很可能是像这样的真实 bug。
一个练习:最大的无符号整数
这是一个很有启发性的小练习。我们如何表示一个常量,它代表能放入 uint
中的最大值?如果我们讨论的是 uint32
而不是 uint
,我们可以写
const MaxUint32 = 1<<32 - 1
但我们想要的是 uint
,而不是 uint32
。 int
和 uint
类型拥有相等但未指定位数的比特,可以是 32 位或 64 位。由于可用比特数取决于架构,我们不能直接写下一个具体的值。
补码算术的拥趸们(Go 的整数被定义为使用补码)都知道,-1
的表示形式是所有位都设置为 1,所以 -1
的位模式在内部与最大的无符号整数相同。因此,我们可能认为可以写
const MaxUint uint = -1 // Error: negative value
但这是非法的,因为 -1 不能用无符号变量表示;-1
不在无符号值的范围内。出于同样的原因,类型转换也无济于事
const MaxUint uint = uint(-1) // Error: negative value
尽管在运行时,-1 的值可以转换为无符号整数,但常量转换的规则禁止这种编译时强制转换。也就是说,这段代码可以工作
var u uint var v = -1 u = uint(v)
但仅因为 v
是一个变量;如果我们将 v
变为常量,即使是无类型常量,我们也将回到禁止的领域
var u uint const v = -1 u = uint(v) // Error: negative value
我们回到之前的方法,但这次不是 -1
,而是尝试 ^0
,这是任意数量零位的按位取反。但这也会失败,原因类似:在数值空间中,^0
代表无限多个 1,所以如果我们将其赋值给任何固定大小的整数,就会丢失信息
const MaxUint uint = ^0 // Error: overflow
那么我们如何将最大的无符号整数表示为常量呢?
关键是将操作限制在 uint
的位数范围内,并避免使用 uint
中不可表示的值,例如负数。最简单的 uint
值是带类型的常量 uint(0)
。如果 uint
有 32 位或 64 位,则 uint(0)
分别有 32 或 64 个零位。如果我们对这些位逐个取反,我们将得到正确数量的一位,这就是最大的 uint
值。
因此,我们不对无类型常量 0
的位进行翻转,而是对有类型常量 uint(0)
的位进行翻转。那么,这就是我们的常量
const MaxUint = ^uint(0) fmt.Printf("%x\n", MaxUint)
无论当前执行环境中(在playground上是 32 位)表示 uint
需要多少位,这个常量都能正确表示类型为 uint
的变量可以容纳的最大值。
如果您理解了得到这个结果的分析过程,那么您就理解了 Go 中所有关于常量的要点。
数字
Go 中无类型常量的概念意味着所有数值常量,无论是整数、浮点数、复数,甚至字符值,都存在于一种统一的空间中。只有当我们把它们带入变量、赋值和操作的计算世界时,实际的类型才变得重要。但是只要我们停留在数值常量的世界里,我们就可以随意混合和匹配值。所有这些常量的数值都是 1
1
1.000
1e3-99.0*10-9
'\x01'
'\u0001'
'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 = '\x01' var p uintptr = '\u0001' 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 中,数值常量的行为符合您的预期:就像数字一样。
下一篇文章:使用 Docker 部署 Go 服务器
上一篇文章:Go 在 OSCON
博客索引