浅析 Go 语言的数字常量

Alex-liutao · · 2945 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

## 概述 Go 语言的常量的实现方法是 Go 的一个亮点。在 Go 的语言规范中的定义[常量规则](http://golang.org/ref/spec#Constants) 是 Go 独有的。 它们在编译器级别提供 Go 所需要的灵活性,使我们编写的代码可读且直观,同时仍保持类型安全。 本文将会探讨”什么是数字常量“、”它们在最简单的情况下时有怎样的行为“以及”怎样去探讨它们才是最好的“这几个方面,其中会有很多吓人的细节问题、名词和概念,所以本文将会放慢速度慢慢的剖析之。 如果你准备好瞧瞧数字常量的底下有些啥,那就跟我撸起袖子开始干吧: ## 无类型和有类型的数字常量 在 Go 语言中,你既可以在声明常量时指定类型,也可以不指定类型。当我们在代码中声明一个字面量时,我们其实就声明了一个匿名的、未指定类型的常量。 下面的例子展示了无类型的、有类型的命名常量和匿名常量: ```go const untypedInteger = 12345 const untypedFloatingPoint = 3.141592 const typedInteger int = 12345 const typedFloatingPoint float64 = 3.141592 ``` 声明的左边就是命名的常量,而右边的字面量其实是匿名的常量。 ## 数字常量的种类 你的第一直觉应该会认为变量的类型系统跟常量的类型系统应该是一致的,但其实不是的。常量如何表示与它们关联的值,有另外的一套实现。所有的 Go 编译器可以自行决定常量的实现方法,只要能满足[这些强制的规定](http://golang.org/ref/spec#Constants)。 当我们声明一个指定类型的常量的时候,声明时指定的类型是用来限定那个常量的精度的,它并不会改变常量的值在底层的数据结构,因为内部用什么数据结构来实现变量的值,不同的编译器有不同的实现方法。所以我们最好是认为,常量拥有的是种类(kind),而不是类型(type) 一个数字常量可以是以下种类之一:整数、浮点数、复数、Unicode 字符(rune): ```go 12345 // kind: 整型 3.141592 // kind: 浮点数 1E6 // kind: 浮点数 ``` 上面的示例中,我们声明了三个数字常量,一个是属于整数种类的,还有俩是属于浮点数种类的。字面量的形式就决定了它是什么种类的常量。当它的形式不包含小数点或者指数的时候,这个常量就是整数种类的。 ## 常量是完全精确的 不管常量是如何实现的,它可以被认为永远都是完全精确(mathematically exact)的。Go 的常量的这个特性是 Go 有别于其它语言的地方。其它语言如 C 或 C++ 并不是这样的。 只要有足够的内存,整数的值永远都会精确地保存。因为 Go 语言规范中要求整数常量至少要有 256 位的精度,我们可以很放心的说整数常量是完全精确的。 要得到完全精确的浮点数,编译器可以采用不用的策略和选项。Go 语言规范没有规定编译器要怎么实现,它只是制定了一系列要满足的条件。 以下是当代 Go 编译器使用的两种实现精确浮点型的策略: - 第一个是将所有浮点数表示为分数,并且对这些分数进行有理数运算,因此这些浮点数永远不会有精度损失的情况。 - 另一个策略是使用精度非常高的浮点数来保存,精度高到足够满足任何用例的精度需求。当这个浮点数是用几百位来保存的时候,这个浮点数基本上相当于是无损失的了。现在的 gc/gccgo 就是用这个方法来实现的。 作为开发者,我们最好还是不要去关心编译器内部是怎么实现的,这个并不重要。只要知道,所有常量,不管他们声明的时候是否指定了类型,在内部都用同样的数据结构来保存他们的值,这点跟变量不一样。而且,常量是完全精确的。 ## 常量完全精确特性的示例 因为常量只存在在编译期间,所以要找到一个证明它们是完全精确的例子还蛮难的。我们可以声明一个比 Go 支持的最大的整形的值还大得多的整数常量,来说明常量的精度是非常大的。 下面的程序可以编译通过,就是因为整数种类的常量是完全精确的 ```go package main import "fmt" // 远远大于 int64 const myConst = 9223372036854775808543522345 func main() { fmt.Println("Will Compile") } ``` 如果我们把上面的常量指定成 `int64` 类型的,那意思是这个常量的范围已经限定在 `int64` 的取值范围以内了,这个时候程序不会编译成功: ```go package main import "fmt" // Much larger value than int64. const myConst int64 = 9223372036854775808543522345 func main() { fmt.Println("Will NOT Compile") } Compiler Error: ./ideal.go:6: constant 9223372036854775808543522345 overflows int64 ``` 从上面我们可以知道,整数种类的常量可以表示非常大的数字。并且能够理解为什么我们说,常量是完全精确的。 ## 数字常量声明 当我们声明一个无类型数字常量的时候,Go 对这个常量的值是怎样的,没有任何的要求: ```go const untypedInteger = 12345 // 种类:整数 const untypedFloatingPoint = 3.141592 // 种类:浮点数 ``` 上面的示例中,左边的常量被赋予与右边的常量相同的值和种类。 当我们声明一个有类型的常量的时候,右边的常量的形式必须要与声明的左边的常量的类型兼容: ```go const typedInteger int = 12345 // 种类:整数 const typedFloatingPoint float64 = 3.141592 // 种类:浮点数 ``` 声明的右边的值也必须在声明的类型的有效范围内。比如说,下面的数字常量的声明是无效的: ```go const myUint8 uint8 = 1000 ``` `uint8` 只能表示 0 到 255 的数字范围。这就是为什么我之前说,声明时指定的类型是用来限定那个常量的精度的。 ## 隐式的整形转换 在 Go 的世界中,变量都不会发生隐式的转换。然而,常量与变量之间的隐式类型转换则经常发生。 我们先来看看隐式的整形转换: ```go var myInt int = 123 ``` 这个示例中,我们有一个整数种类的常量 `123`,它被隐式地转换成 `int` 类型。因为这个常量的字面值中没有小数点或者指数,这个常量将会作为一个整数种类的常量。整数种类的常量可以隐式地转换成有符号或者无符号的、任意长度的变量,只要整个过程没有发生数据的溢出。 浮点数的种类的常量也可以隐式地转换成整型变量,只要这个常量的值的形式是可以兼容整型的: ```go var myInt int = 123.0 ``` 我们还可以进行隐式的常量 - 变量转换时,省略变量的类型指定: ```go var myInt = 123 ``` 在这个例子中,当我们用值为 `123` 的整数常量来初始化 `myInt` 变量时,`myInt` 会隐式地采用默认的 `int64` 类型。 ## 隐式的浮点类型转换 我们接着来看看隐式的浮点型转换: ```go var myFloat float64 = 0.333 ``` 这时候编译器会执行一个隐式转换,从浮点数种类的常量 `0.333` 转换为 `float64` 类型的浮点型变量。因为常量字面值中含有一个小数点,所以这个常量是一个浮点数种类的常量。默认情况下,会把一个浮点数种类的常量转换为 `float64` 类型的变量。 编译器还可以把整数类型的常量隐式的转换成为 `float64` 类型的变量。 ```go var myFloat float64 = 1 ``` 在这个示例中,整数常量 `1` 被隐式地转换成 `float64` 类型的变量了。 ## 种类提升 我们写程序是经常要执行一个常量与另一个常量或者变量的算术运算。它服从 Go 语言规范中[二元运算](http://golang.org/ref/spec#Operators) 的规则。规则中指明操作符两边的操作数的类型必须相同,除非操作涉及移位或者无类型的常量。 我们来看看下面两个常量相乘的例子: ```go var answer = 3 * 0.333 ``` 在这个示例中,我们执行了一个整数常量 `3` 与浮点数常量 `0.333` 的乘法。 在 Go 语言规范中,有一条规则规定了这种操作: > ”除了移位运算,如果二元运算的操作数是不同种类的无类型常量,...,运算结果使用以下种类中最靠后的一个:整数、Unicode 字符、浮点数、复数。“ 根据这个规则,例子中两个常量的乘法运算结果将会是浮点数类型的。浮点种类被提升到整数种类之前。 ## 数字常量运算 我们继续来讨论我们的乘法例子: ```go var answer = 3 * 0.333 ``` 这个乘法的结果是一个浮点种类的**常量**,这个常量再接着通过一个隐式的类型转换赋值到 `answer` 变量中,这个 `answer` 变量的类型是 `float64`。 当我们使两个常量相除的时候,常量的种类决定了这个除法运算要如何进行。 ```go const third = 1 / 3.0 ``` 当两个常量中其中一个是浮点数种类的常量的时候。这个除法运算的结果也会是一个浮点数种类的常量。在我们的例子中,我们的除数中含有小数点,所以它是一个浮点数类型的常量,进而它也满足我们前面提到的常量提升的规则。 让我们把上面的例子中的除数改成整数常量: ```go const zero = 1 / 3 ``` 这次我们参与除法运算的两个常量都是整数常量。运算的结果会是一个新的整数常量。因为 1 除以 3 小于 1,所以结果将会是一个整数常量 `0`。 我们来通过算术运算创建一个有类型的常量: ```go type Numbers int8 const One Numbers = 1 const Two = 2 * One ``` 这个例子中, 我们声明了一个名为 `Numbers` 的类型,这个类型的底层类型是 `int8` ,然后我们声明一个指定为 `Number` 类型的整数常量 `One` ,它的值为 `1`。然后我们声明一个名为 `Two` 的常量,它会在整数常量 `2` 与 `One` 常量相乘之后提升为类型为 `Number` 的常量。 `Two` 的声明是个很好的例子,它演示了常量不仅可以被提升到用户定义类型,而且可以被提升为与某个基类型相关联的用户定义类型的常量。 ## 我们的实战示例 我们来从标准库里面看看一个实战的示例。`time` 包声明了这个类型,和一系列的常量: ```go type Duration int64 const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond ) ``` 上面所有的常量声明都是 `Duration` 类型的,它的基类型是 `int64`。这里我们通过一个有类型常量和无类型的常量间的算术运算来声明一个有类型的常量。 因为编译器会为常量执行隐式的转换。我们可以在 Go 里面这样写代码: ```go package main import ( "fmt" "time" ) const fiveSeconds = 5 * time.Second func main() { now := time.Now() LessFiveNanoseconds := now.Add(-5) LessFiveSeconds := now.Add(-fiveSeconds) fmt.Printf("Now : %v\n", now) fmt.Printf("Nano : %v\n", LessFiveNanoseconds) fmt.Printf("Seconds : %v\n", LessFiveSeconds) } // Output: // Now : 2014-03-27 13:30:49.111038384 -0400 EDT // Nano : 2014-03-27 13:30:49.111038379 -0400 EDT // Seconds : 2014-03-27 13:30:44.111038384 -0400 EDT ``` 常量的超能力通过 `Add` 函数很好的展示了出来,我们看到,`Time` 中 `Add` 方法的定义如下: ```go func (t Time) Add(d Duration) Time ``` `Add` 方法接受一个类型为 `Duration` 的参数。我们再看看刚才我们的程序中调用到 `Add` 方法的地方: ```go var LessFiveNanoseconds = now.Add(-5) var LessFiveMinutes = now.Add(-fiveSeconds) ``` 编译器隐式地把常量 `-5` 转换成 `Duration` 类型的变量,使得方法能够成功的调用。常量 `fiveSeconds` 已经是 `Duration` 类型的常量了,这要归功于常量算术的规则: ```go const fiveSeconds = 5 * time.Second ``` 常量 `5` 与 `time.Second` 的结果,使得常量 `fiveSecond` 成为一个类型为 `Duration` 的常量。这是因为 `time.Second` 常量是一个 `Duration` 类型的的常量,在决定算术结果的类型的时候,`time.Second` 常量的类型被提升了。为了使得 `Add` 方法能够成功调用,这个常量还要进一步地从 `Duartion` 类型的**常量**隐式转换成为 `Duartion` 类型的**变量**。 如果常量没有了现在这些特性的话,我们上面的各种赋值与函数调用都要显式地转换一下。看看如果我们尝试用一个 `int` 型的值去做相应的方法调用: ```go var difference int = -5 var LessFiveNano = now.Add(difference) Compiler Error: ./const.go:16: cannot use difference (type int) as type time.Duration in function argument ``` 一旦我们使用一个有类型的整形值作为 `Add` 函数调用的参数的时候,我们会收到一个编译错误。编译器不会允许有类型的变量进行隐式的类型转换。要想上面的代码编译通过的话,我们需要执行一个显式的类型转换: ```go Add(time.Duration(difference)) ``` 常量是唯一不用我们执行显式类型转换的机制。 ## 结论 我们对常量的行为习以为常,这恰恰正好就是语言的设计者和实现这个特性的人辛勤劳动的最好证明。为了让常量能够像现在这样方便,并且希望它能为使用者带来便利,工作人员投入了很多心血和努力。 所以下一次你在用一个常量的时候,记住你现在使用的是一个特殊的,隐藏在编译器里面的瑰宝,尽管它并没有被人广泛的发掘出来。常量让 Go 编程更有乐趣、可读性更强以及更直观,并且同时还能保证我们写的代码是类型安全的。 ## 感谢 感谢 [Nate Finch](https://www.ardanlabs.com/broken-link) 和 [Kim Shrier](http://www.westryn.net/resumes/kim.html) 为我的文章进行了多次的校对,这让我的文章的内容和示例更加准确,流畅并且更加有趣。我有很多次都打算放弃了,是 Nate 的鼓励使我继续写下去。 特别要感谢 Robert Griesemer 和 Go 开发团队的开发者付出它们的时间和耐心指导我一些细节的问题。Go 开发团队的成员都非常的厉害,并且对整个社区还有社区的成员都非常关心。由衷感谢!

via: https://www.ardanlabs.com/blog/2014/04/introduction-to-numeric-constants-in-go.html

作者:William Kennedy  译者:Alex-liutao  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2945 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
下一篇:Go GC
1 回复  |  直到 2019-06-25 16:24:36
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传