Go语言中的常量 - Go语言中文社区

Go语言中的常量


本文翻译并节选自Constants

Go是一种静态类型语言,不允许将不同类型的数字混合在同一个表达式中。你不能将一个 float64 类型的值加到 int 类型上去,甚至不能写出 int32 + int 这样的表达式。但是,如下的表达式都是合法的:

  • 1e6 * time.Second
  • math.Exp(1)
  • 1 << ('t' + 2.0)

在Go语言中,常量和变量是不同的,其行为与常规的数字非常相似。这篇文章将解释为什么会这样。

C语言背景

在Go语言发展的早期阶段,我们讨论了由C及其衍生语言中混合和匹配数字类型而引起的一些问题。许多神秘的错误,崩溃和可移植性问题是由混合了不同小大和"符号"的整数的表达式所引起的。虽然经验丰富的C程序员对于以下计算结果非常熟悉:

unsigned int u = 1e9;
long signed int i = -1;
... i + u ...

但是结果并不明显:值会占用多大空间?具体的结果是多少?结果是否带符号?

讨厌的bugs就潜伏在这里。

C有一套称为"通用算术转换"的规则,它用于指定编译器如何自动转化不同类型的数字(引入了更多的bug)。

在设计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中,const是一个关键字,它引入标量值的名称,例如2或者3.14159或"scrumptious",这些名称或其他值在Go中称为常量。常量也可以通过由常量构建的表达式定义,例如2 + 3或2 + 3i或math.Pi / 2或 ("go" + "pher")。

有些语言没有常量,有些语言对常量或应用单词const有更一般的含义。例如,在C和C++中,const是一个类型限定符,可以编写更复杂的属性或者更复杂的值。

但是在Go中,常量 只是一个简单、不变的值,从这里开始,我们只讨论Go。

字符串常量

有许多种数字常量 - 整数,浮点数,runes,有符号数,无符号数,虚数,复数。所以让我们从一个更简单的常量形式开始:字符串。字符串常量易于理解,并提供了一个较小的空间来探索Go中常量的类型问题。

字符串常量是用双引号括起的一些文本(Go也有原始字符串文字,用反引号括起来,但在讨论常量时,他们具有相同的属性)。这是一个字符串常量:

"Hello, 世界"

这个字符串常量的类型是什么?显而易见的答案是string,但这是错误的。

这是一个无类型的字符串常量,也就是说,它是一个尚未具有固定类型的常量文本值。是的,它是一个字符串,但它不是一个具有string类型的Go value。即使给出一个名字,它仍然是一个无类型的字符串常量:

const hello = "Hello, 世界"

在此声明之后,hello也是一个无类型的字符串常量。无类型常量只是一个值,一个尚未给定定义类型的值会强制遵守阻止组合不同类型值的规则。

正是这种无类型常量的概念使得我们能够以极大的自由在Go中使用常量。

那么,什么是类型化的字符串常量?这是一个示例:

const typedHello string = "Hello, 世界"

请注意,typedHello的声明在等号前面具有显式字符串类型。这意味着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("%T: %vn", "Hello, 世界", "Hello, 世界")
fmt.Printf("%T: %vn", hello, hello)

如果常量拥有一个类型,那么它会保存在接口中,如下所示:

fmt.Printf("%T: %vn", myStringHello, myStringHello)

总之,类型化常量遵循Go中所有类型值的规则。另一方面,无类型常量不以相同的方式携带Go类型,并且可以更自由地混合和匹配。但是,它确实有一个默认类型,当且仅当没有其他类型信息可用时才会公开。

由语法决定的默认类型

无类型常量的默认类型由其语法决定。对于字符串常量,唯一可能的默认类型是string。对于数字常量,默认类型具有更多种类。整数常量默认为int,浮点数常量默认为float64,runes常量默认为rune(int32的别名),虚数常量默认为complex128。这里是我们用于显示默认类型的标准语句:

    fmt.Printf("%T %vn", 0, 0)
    fmt.Printf("%T %vn", 0.0, 0.0)
    fmt.Printf("%T %vn", 'x', 'x')
    fmt.Printf("%T %vn", 0i, 0i)
Booleans

我们所说的关于无类型字符串常量的所有内容都可以套用在无类型布尔常量上。值truefalse是可以分配给任何布尔变量的无类型布尔常量,但是一旦给定类型,布尔变量就不能混合:

    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)
Floats

在大多数方面,浮点常量就像布尔常量一样:

    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

    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)

编译器给出的错误是:constant 1e+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 或其他更复杂的计算在分配结果之前可以拥有更高的精度,使得涉及常量的计算更容易编写而不会丢失精度。这也意味着不存在像无穷大,软下溢和NaN这样的情况在常数表达式中出现。(除以常数0是编译错误)

Complex numbers

复数常量的行为与浮点常量非常相似:

    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: %vn", s, s)

打印:complex128: (2+0i)

但是从数学上讲,Two可以存储在float64float32中而不会丢失精度。因此,我们可以在初始化赋值中将Two分配给float64,而不会出现问题:

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

输出:2 and 2。即使Two是一个复数常量,它仍然可以分配给一个浮点变量。像这样的常量“交叉”类型的这种能力将证明是有用的。

Integers

最后我们来讨论整数。它们拥有更多的种类 - 多种大小,符号或无符号等等。但它们遵循相同但规则:

    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

如上所述,整数有两种形式,每种形式都有自己的默认类型:int 表示简单常量,如123或0xFF或-14,rune表示字符,如'a','世'或'r'。

没有无类型常量的默认类型是无符号整数。但是,无类型常量的灵活性意味着只要我们清楚类型,我们就可以使用简单常量初始化无符号整数变量。它类似于我们如何使用无虚数部分的复数来初始化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。

版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/93349ed73bce
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-09 21:51:18
  • 阅读 ( 1455 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢