Go 博客

常量

Rob Pike
2014 年 8 月 25 日

介绍

Go 是一种静态类型语言,不允许混合使用数字类型进行运算。您不能将 float64 添加到 int,甚至不能将 int32 添加到 int。但是,编写 1e6*time.Secondmath.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 时,我们决定通过强制混合使用数字类型来避免这个雷区。如果您想添加 iu,您必须明确说明您想要的结果。给定

var u uint
var i int

您可以编写 uint(i)+ui+int(u),这两种方法都明确表达了加法的含义和类型,但与 C 不同,您不能编写 i+u。您甚至不能混合使用 intint32,即使 int 是一个 32 位类型。

这种严格性消除了一个常见的 bug 和其他故障原因。它是 Go 的一项重要属性。但它也有代价:有时它要求程序员使用笨拙的数字转换来装饰他们的代码,以便清楚地表达他们的意思。

那么常量呢?给定上面的声明,什么会使编写 i = 0u = 0 成为合法?0类型是什么?要求常量在简单的上下文中(如 i = int(0))具有类型转换是不合理的。

我们很快意识到答案在于让数字常量与它们在其他类似 C 的语言中的行为方式不同。经过大量的思考和实验,我们提出了一种我们认为几乎总是感觉正确的设计,它使程序员免于一直进行常量转换,但能够编写像 math.Sqrt(2) 这样的内容,而不会受到编译器的责备。

简而言之,Go 中的常量通常都可以正常工作。让我们看看它是如何实现的。

术语

首先,快速定义一下。在 Go 中,const 是一个关键字,用于引入一个表示标量值的名称,例如 23.14159"scrumptious"。这些值,无论是命名的还是未命名的,在 Go 中都称为常量。常量也可以通过从常量构建的表达式创建,例如 2+32+3imath.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 变量。也就是说,这段代码可以正常工作


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {

    var s string
    s = typedHello
    fmt.Println(s)
}

但这不能正常工作


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {

    type MyString string
    var m MyString
    m = typedHello // Type error
    fmt.Println(m)
}

变量 m 的类型为 MyString,不能赋值为不同类型的变量。它只能赋值为类型为 MyString 的变量,例如


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {
    type MyString string
    var m MyString

    const myStringHello MyString = "Hello, 世界"
    m = myStringHello // OK
    fmt.Println(m)
}

或通过强制转换来解决问题,例如


package main

import "fmt"

const typedHello string = "Hello, 世界"

func main() {
    type MyString string
    var m MyString

    m = MyString(typedHello)
    fmt.Println(m)
}

回到我们的无类型字符串常量,它有一个有用的属性,即由于它没有类型,所以将它赋值给一个有类型变量不会导致类型错误。也就是说,我们可以写

m = "Hello, 世界"

m = hello

因为与有类型常量 typedHellomyStringHello 不同,无类型常量 "Hello, 世界"hello没有类型。将它们赋值给任何与字符串兼容的类型变量都不会导致错误。

这些无类型字符串常量当然都是字符串,因此它们只能在允许使用字符串的地方使用,但它们没有类型 string

默认类型

作为一名 Go 程序员,您肯定见过许多像这样的声明

str := "Hello, 世界"

现在您可能会问,“如果常量是无类型的,那么 str 在这个变量声明中是如何获得类型的?”答案是,无类型常量有一个默认类型,它是一个隐式类型,如果在没有提供类型的情况下需要类型,它会将该类型传递给一个值。对于无类型字符串常量,该默认类型显然是 string,因此

str := "Hello, 世界"

var str = "Hello, 世界"

与以下内容完全相同

var str string = "Hello, 世界"

您可以将无类型常量理解为存在于某种理想值空间中,这个空间比 Go 的完整类型系统限制更少。但是要对它们做任何事情,我们需要将它们赋值给变量,当这样做时,变量(而不是常量本身)需要一个类型,并且常量可以告诉变量它应该是什么类型。在这个示例中,str 成为一个类型为 string 的值,因为无类型字符串常量为声明提供了其默认类型 string

在这种声明中,使用类型和初始值来声明变量。然而,有时当我们使用常量时,值的目的地并不那么清楚。例如,考虑以下语句


package main

import "fmt"

func main() {

    fmt.Printf("%s", "Hello, 世界")
}

fmt.Printf 的签名是

func Printf(format string, a ...interface{}) (n int, err error)

也就是说,它的参数(在格式字符串之后)是接口值。当 fmt.Printf 使用无类型常量调用时,会创建一个接口值作为参数传递,并且为该参数存储的具体类型是常量的默认类型。这个过程类似于我们之前在使用无类型字符串常量声明一个初始化值时看到的情况。

您可以在这个示例中看到结果,它使用格式 %v 打印值,使用 %T 打印传递给 fmt.Printf 的值的类型


package main

import "fmt"

const hello = "Hello, 世界"

func main() {

    fmt.Printf("%T: %v\n", "Hello, 世界", "Hello, 世界")
    fmt.Printf("%T: %v\n", hello, hello)
}

如果常量有类型,则该类型将进入接口,如以下示例所示


package main

import "fmt"

type MyString string

const myStringHello MyString = "Hello, 世界"

func main() {

    fmt.Printf("%T: %v\n", myStringHello, myStringHello)
}

(有关接口值工作方式的更多信息,请参阅 这篇博文 的第一部分。)

总之,有类型常量遵循 Go 中所有有类型值的规则。另一方面,无类型常量不会像有类型常量那样携带 Go 类型,并且可以更自由地混合使用。但是,它确实有一个默认类型,该类型会在没有其他类型信息可用时才会暴露出来。

由语法决定的默认类型

未类型化常量的默认类型由其语法决定。对于字符串常量,唯一可能的隐式类型是string。对于数值常量,隐式类型更加多样。整数常量的默认类型为int,浮点数常量的默认类型为float64,字符常量的默认类型为runeint32的别名),虚数常量的默认类型为complex128。以下是我们反复使用的标准打印语句,用于展示默认类型在实际中的作用


package main

import "fmt"

func main() {

    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'的结果。)

布尔值

关于未类型化字符串常量的所有内容都适用于未类型化布尔常量。truefalse是未类型化布尔常量,可以赋值给任何布尔变量,但一旦赋予类型,布尔变量就不能混用


package main

import "fmt"

func main() {

    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”行并再次运行。这里的模式与字符串常量完全一致。

浮点数

浮点数常量在大多数方面与布尔常量类似。我们的标准示例在转换中按预期工作


package main

import "fmt"

func main() {

    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中有两种浮点类型:float32float64。浮点数常量的默认类型为float64,尽管未类型化浮点数常量可以很好地赋值给float32


package main

import "fmt"

func main() {
    const Zero = 0.0
    const TypedZero float64 = 0.0

    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

——毕竟这只是一个数字——但我们不能赋值或打印它。该语句甚至无法编译


package main

import "fmt"

func main() {
    const Huge = 1e1000

    fmt.Println(Huge)
}

错误是,“常量1.00000e+1000 溢出 float64”,这是正确的。但Huge可能很有用:我们可以在包含其他常量的表达式中使用它,并使用这些表达式的值(如果结果可以在float64的范围内表示)。语句,


package main

import "fmt"

func main() {
    const Huge = 1e1000

    fmt.Println(Huge / 1e999)
}

打印10,正如预期的那样。

类似地,浮点数常量可能具有非常高的精度,因此涉及它们的算术运算更加精确。在math包中定义的常量比float64中提供的位数更多。以下是math.Pi的定义

Pi  = 3.14159265358979323846264338327950288419716939937510582097494459

当该值被赋值给变量时,一些精度将丢失;赋值将创建最接近高精度值的float64(或float32)值。这段代码


package main

import (
    "fmt"
    "math"
)

func main() {

    pi := math.Pi
    fmt.Println(pi)
}

打印3.141592653589793

具有如此多的位数意味着像Pi/2或其他更复杂的计算可以保留更多的精度,直到结果被赋值,这使得涉及常量的计算更容易编写,而不会丢失精度。这也意味着在常量表达式中不会出现浮点边缘情况,例如无穷大、软下溢和NaN。(除以常量零是编译时错误,当一切都只是数字时,就没有“非数字”这种东西。)

复数

复数常量与浮点数常量非常相似。以下是我们现在熟悉的列表的复数版本


package main

import "fmt"

func main() {

    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.0i1.0i,甚至1i

让我们玩个把戏。我们知道在 Go 中,数值常量只是一个数字。如果这个数字是一个没有虚部的复数,也就是说,一个实数呢?以下是一个例子

    const Two = 2.0 + 0i

这是一个未类型化复数常量。即使它没有虚部,表达式的语法也将其定义为具有默认类型complex128。因此,如果我们使用它来声明一个变量,默认类型将是complex128。这段代码


package main

import "fmt"

func main() {
    const Two = 2.0 + 0i

    s := Two
    fmt.Printf("%T: %v\n", s, s)
}

打印complex128: (2+0i)。但从数值上讲,Two可以存储在标量浮点数中,即float64float32中,而不会丢失信息。因此,我们可以将Two赋值给float64,无论是在初始化中还是在赋值中,都不会出现问题


package main

import "fmt"

func main() {
    const Two = 2.0 + 0i

    var f float64
    var g float64 = Two
    f = Two
    fmt.Println(f, "and", g)
}

输出是2 and 2。即使Two是一个复数常量,它也可以赋值给标量浮点变量。这种常量“跨越”类型的能力将证明非常有用。

整数

最后我们来谈谈整数。它们有更多的活动部件——多种大小、有符号或无符号,等等——但它们遵循相同的规则。最后一次,以下是我们熟悉的示例,这次只使用int


package main

import "fmt"

func main() {

    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

(加上byteuint8的别名和runeint32的别名)。这很多,但常量工作方式的模式现在应该足够熟悉,以至于你可以看到事情是如何发展的。

如上所述,整数有两种形式,每种形式都有自己的默认类型:int用于像1230xFF-14这样的简单常量,以及rune用于像'a'、'世'或'\r'这样的带引号的字符。

没有常量形式的默认类型是无符号整数类型。但是,未类型化常量的灵活性意味着我们可以使用简单常量来初始化无符号整数变量,只要我们明确类型即可。这类似于我们可以使用没有虚部的复数来初始化float64。以下几种初始化uint的不同方法;它们都是等效的,但所有方法都必须显式地提到类型,以便结果是无符号的。

var u uint = 17
var u = uint(17)
u := uint(17)

类似于浮点数部分提到的范围问题,并非所有整数值都适合所有整数类型。可能会出现两种问题:值可能太大,或者它可能是一个负值,被赋值给无符号整数类型。例如,int8的范围是 -128 到 127,因此超出该范围的常量永远不能赋值给类型为int8的变量


package main

func main() {

    var i8 int8 = 128 // Error: too large.
   _ = i8
}

类似地,uint8,也称为byte,的范围是 0 到 255,因此大的或负的常量不能赋值给uint8


package main

func main() {

    var u8 uint8 = -1 // Error: negative value.
   _ = u8
}

这种类型检查可以捕获像这样的错误


package main

func main() {

    type Char byte
    var c Char = '世' // Error: '世' has value 0x4e16, too large.
   _ = c
}

如果编译器抱怨你使用常量的方式,这很可能是一个真正的错误,比如这个。

练习:最大的无符号 int

这是一个很有启发性的练习。我们如何表达一个常量,它代表适合uint的最大值?如果我们谈论的是uint32而不是uint,我们可以写

const MaxUint32 = 1<<32 - 1

但我们想要uint,而不是uint32intuint类型具有相同的未指定位数,要么是 32 位,要么是 64 位。由于可用位数取决于体系结构,我们不能仅仅写下一个单一的值。

熟悉补码运算(Go 的整数被定义为使用补码运算)的人知道,-1的表示法是其所有位都被设置为 1,因此-1的位模式在内部与最大无符号整数的位模式相同。因此,我们可能认为我们可以写


package main

func main() {

    const MaxUint uint = -1 // Error: negative value
}

但这是非法的,因为 -1 不能由无符号变量表示;-1不在无符号值的范围内。转换也没有帮助,原因相同


package main

func main() {

    const MaxUint uint = uint(-1) // Error: negative value
}

即使在运行时,-1 的值可以转换为无符号整数,但常量转换的规则在编译时禁止这种强制转换。也就是说,这可以工作


package main

func main() {

    var u uint
    var v = -1
    u = uint(v)
   _ = u
}

但这仅仅是因为v是一个变量;如果我们把v变成一个常量,即使是一个未类型化常量,我们也会回到禁止领域


package main

func main() {

    var u uint
    const v = -1
    u = uint(v) // Error: negative value
   _ = u
}

我们回到之前的方法,但我们尝试的是^0,而不是-1,它是任意数量的零位的按位取反。但这也会失败,原因类似:在数值空间中,^0表示无限个 1,因此如果我们将它赋值给任何固定大小的整数,我们就会丢失信息


package main

func main() {

    const MaxUint uint = ^0 // Error: overflow
}

那么我们如何将最大的无符号整数表示为常量呢?

关键是将操作限制在uint的位数内,并避免无法在uint中表示的值,例如负数。最简单的uint值是类型化常量uint(0)。如果uints有 32 位或 64 位,那么uint(0)相应地有 32 个或 64 个零位。如果我们反转这些位的每一个,我们将得到正确的 1 位数量,这是最大的uint值。

因此,我们不反转未类型化常量0的位,而是反转类型化常量uint(0)的位。因此,以下是我们的常量


package main

import "fmt"

func main() {

    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

因此,尽管它们有不同的隐式默认类型,但作为未类型化常量,它们可以赋值给任何数值类型的变量


package main

import "fmt"

func main() {

    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

你甚至可以做一些奇怪的事情,比如


package main

import "fmt"

func main() {

    var f = 'a' * 1.5
    fmt.Println(f)
}

这将得到 145.5,这除了证明一个观点之外毫无意义。

但这些规则的真正意义在于灵活性。这种灵活性意味着,尽管在 Go 中,在一个表达式中混合浮点数和整数变量,甚至 intint32 变量是非法的,但编写

sqrt2 := math.Sqrt(2)

const millisecond = time.Second/1e3

bigBufferWithHeader := make([]byte, 512+1e6)

并使其结果符合你的预期是完全可以的。

因为在 Go 中,数字常量就像数字一样,按预期工作。

下一篇文章:使用 Docker 部署 Go 服务器
上一篇文章:OSCON 上的 Go
博客索引