浅析 Go 语言的数字常量

栏目: Go · 发布时间: 5年前

内容简介:Go 语言的常量的实现方法是 Go 的一个亮点。在 Go 的语言规范中的定义本文将会探讨”什么是数字常量“、”它们在最简单的情况下时有怎样的行为“以及”怎样去探讨它们才是最好的“这几个方面,其中会有很多吓人的细节问题、名词和概念,所以本文将会放慢速度慢慢的剖析之。如果你准备好瞧瞧数字常量的底下有些啥,那就跟我撸起袖子开始干吧:

概述

Go 语言的常量的实现方法是 Go 的一个亮点。在 Go 的语言规范中的定义 常量规则 是 Go 独有的。 它们在编译器级别提供 Go 所需要的灵活性,使我们编写的代码可读且直观,同时仍保持类型安全。

本文将会探讨”什么是数字常量“、”它们在最简单的情况下时有怎样的行为“以及”怎样去探讨它们才是最好的“这几个方面,其中会有很多吓人的细节问题、名词和概念,所以本文将会放慢速度慢慢的剖析之。

如果你准备好瞧瞧数字常量的底下有些啥,那就跟我撸起袖子开始干吧:

无类型和有类型的数字常量

在 Go 语言中,你既可以在声明常量时指定类型,也可以不指定类型。当我们在代码中声明一个字面量时,我们其实就声明了一个匿名的、未指定类型的常量。

下面的例子展示了无类型的、有类型的命名常量和匿名常量:

const untypedInteger       = 12345
const untypedFloatingPoint = 3.141592

const typedInteger int           = 12345
const typedFloatingPoint float64 = 3.141592

声明的左边就是命名的常量,而右边的字面量其实是匿名的常量。

数字常量的种类

你的第一直觉应该会认为变量的类型系统跟常量的类型系统应该是一致的,但其实不是的。常量如何表示与它们关联的值,有另外的一套实现。所有的 Go 编译器可以自行决定常量的实现方法,只要能满足 这些强制的规定

当我们声明一个指定类型的常量的时候,声明时指定的类型是用来限定那个常量的精度的,它并不会改变常量的值在底层的数据结构,因为内部用什么数据结构来实现变量的值,不同的编译器有不同的实现方法。所以我们最好是认为,常量拥有的是种类(kind),而不是类型(type)

一个数字常量可以是以下种类之一:整数、浮点数、复数、Unicode 字符(rune):

12345    // kind: 整型
3.141592 // kind: 浮点数
1E6      // kind: 浮点数

上面的示例中,我们声明了三个数字常量,一个是属于整数种类的,还有俩是属于浮点数种类的。字面量的形式就决定了它是什么种类的常量。当它的形式不包含小数点或者指数的时候,这个常量就是整数种类的。

常量是完全精确的

不管常量是如何实现的,它可以被认为永远都是完全精确(mathematically exact)的。Go 的常量的这个特性是 Go 有别于其它语言的地方。其它语言如 C 或 C++ 并不是这样的。

只要有足够的内存,整数的值永远都会精确地保存。因为 Go 语言规范中要求整数常量至少要有 256 位的精度,我们可以很放心的说整数常量是完全精确的。

要得到完全精确的浮点数,编译器可以采用不用的策略和选项。Go 语言规范没有规定编译器要怎么实现,它只是制定了一系列要满足的条件。

以下是当代 Go 编译器使用的两种实现精确浮点型的策略:

  • 第一个是将所有浮点数表示为分数,并且对这些分数进行有理数运算,因此这些浮点数永远不会有精度损失的情况。
  • 另一个策略是使用精度非常高的浮点数来保存,精度高到足够满足任何用例的精度需求。当这个浮点数是用几百位来保存的时候,这个浮点数基本上相当于是无损失的了。现在的 gc/gccgo 就是用这个方法来实现的。

作为开发者,我们最好还是不要去关心编译器内部是怎么实现的,这个并不重要。只要知道,所有常量,不管他们声明的时候是否指定了类型,在内部都用同样的数据结构来保存他们的值,这点跟变量不一样。而且,常量是完全精确的。

常量完全精确特性的示例

因为常量只存在在编译期间,所以要找到一个证明它们是完全精确的例子还蛮难的。我们可以声明一个比 Go 支持的最大的整形的值还大得多的整数常量,来说明常量的精度是非常大的。

下面的程序可以编译通过,就是因为整数种类的常量是完全精确的

package main

import "fmt"

// 远远大于 int64
const myConst = 9223372036854775808543522345

func main() {
    fmt.Println("Will Compile")
}

如果我们把上面的常量指定成 int64 类型的,那意思是这个常量的范围已经限定在 int64 的取值范围以内了,这个时候程序不会编译成功:

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 对这个常量的值是怎样的,没有任何的要求:

const untypedInteger       = 12345    // 种类:整数
const untypedFloatingPoint = 3.141592 // 种类:浮点数

上面的示例中,左边的常量被赋予与右边的常量相同的值和种类。

当我们声明一个有类型的常量的时候,右边的常量的形式必须要与声明的左边的常量的类型兼容:

const typedInteger int           = 12345    // 种类:整数
const typedFloatingPoint float64 = 3.141592 // 种类:浮点数

声明的右边的值也必须在声明的类型的有效范围内。比如说,下面的数字常量的声明是无效的:

const myUint8 uint8 = 1000

uint8 只能表示 0 到 255 的数字范围。这就是为什么我之前说,声明时指定的类型是用来限定那个常量的精度的。

隐式的整形转换

在 Go 的世界中,变量都不会发生隐式的转换。然而,常量与变量之间的隐式类型转换则经常发生。

我们先来看看隐式的整形转换:

var myInt int = 123

这个示例中,我们有一个整数种类的常量 123 ,它被隐式地转换成 int 类型。因为这个常量的字面值中没有小数点或者指数,这个常量将会作为一个整数种类的常量。整数种类的常量可以隐式地转换成有符号或者无符号的、任意长度的变量,只要整个过程没有发生数据的溢出。

浮点数的种类的常量也可以隐式地转换成整型变量,只要这个常量的值的形式是可以兼容整型的:

var myInt int = 123.0

我们还可以进行隐式的常量 - 变量转换时,省略变量的类型指定:

var myInt = 123

在这个例子中,当我们用值为 123 的整数常量来初始化 myInt 变量时, myInt 会隐式地采用默认的 int64 类型。

隐式的浮点类型转换

我们接着来看看隐式的浮点型转换:

var myFloat float64 = 0.333

这时候编译器会执行一个隐式转换,从浮点数种类的常量 0.333 转换为 float64 类型的浮点型变量。因为常量字面值中含有一个小数点,所以这个常量是一个浮点数种类的常量。默认情况下,会把一个浮点数种类的常量转换为 float64 类型的变量。

编译器还可以把整数类型的常量隐式的转换成为 float64 类型的变量。

var myFloat float64 = 1

在这个示例中,整数常量 1 被隐式地转换成 float64 类型的变量了。

种类提升

我们写程序是经常要执行一个常量与另一个常量或者变量的算术运算。它服从 Go 语言规范中 二元运算 的规则。规则中指明操作符两边的操作数的类型必须相同,除非操作涉及移位或者无类型的常量。

我们来看看下面两个常量相乘的例子:

var answer = 3 * 0.333

在这个示例中,我们执行了一个整数常量 3 与浮点数常量 0.333 的乘法。

在 Go 语言规范中,有一条规则规定了这种操作:

”除了移位运算,如果二元运算的操作数是不同种类的无类型常量,...,运算结果使用以下种类中最靠后的一个:整数、Unicode 字符、浮点数、复数。“

根据这个规则,例子中两个常量的乘法运算结果将会是浮点数类型的。浮点种类被提升到整数种类之前。

数字常量运算

我们继续来讨论我们的乘法例子:

var answer = 3 * 0.333

这个乘法的结果是一个浮点种类的 常量 ,这个常量再接着通过一个隐式的类型转换赋值到 answer 变量中,这个 answer 变量的类型是 float64

当我们使两个常量相除的时候,常量的种类决定了这个除法运算要如何进行。

const third = 1 / 3.0

当两个常量中其中一个是浮点数种类的常量的时候。这个除法运算的结果也会是一个浮点数种类的常量。在我们的例子中,我们的除数中含有小数点,所以它是一个浮点数类型的常量,进而它也满足我们前面提到的常量提升的规则。

让我们把上面的例子中的除数改成整数常量:

const zero = 1 / 3

这次我们参与除法运算的两个常量都是整数常量。运算的结果会是一个新的整数常量。因为 1 除以 3 小于 1,所以结果将会是一个整数常量 0

我们来通过算术运算创建一个有类型的常量:

type Numbers int8
const One Numbers = 1
const Two         = 2 * One

这个例子中, 我们声明了一个名为 Numbers 的类型,这个类型的底层类型是 int8 ,然后我们声明一个指定为 Number 类型的整数常量 One ,它的值为 1 。然后我们声明一个名为 Two 的常量,它会在整数常量 2One 常量相乘之后提升为类型为 Number 的常量。

Two 的声明是个很好的例子,它演示了常量不仅可以被提升到用户定义类型,而且可以被提升为与某个基类型相关联的用户定义类型的常量。

我们的实战示例

我们来从标准库里面看看一个实战的示例。 time 包声明了这个类型,和一系列的常量:

type Duration int64

const (
    Nanosecond Duration = 1
    Microsecond         = 1000 * Nanosecond
    Millisecond         = 1000 * Microsecond
    Second              = 1000 * Millisecond
)

上面所有的常量声明都是 Duration 类型的,它的基类型是 int64 。这里我们通过一个有类型常量和无类型的常量间的算术运算来声明一个有类型的常量。

因为编译器会为常量执行隐式的转换。我们可以在 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 函数很好的展示了出来,我们看到, TimeAdd 方法的定义如下:

func (t Time) Add(d Duration) Time

Add 方法接受一个类型为 Duration 的参数。我们再看看刚才我们的程序中调用到 Add 方法的地方:

var LessFiveNanoseconds = now.Add(-5)
var LessFiveMinutes = now.Add(-fiveSeconds)

编译器隐式地把常量 -5 转换成 Duration 类型的变量,使得方法能够成功的调用。常量 fiveSeconds 已经是 Duration 类型的常量了,这要归功于常量算术的规则:

const fiveSeconds = 5 * time.Second

常量 5time.Second 的结果,使得常量 fiveSecond 成为一个类型为 Duration 的常量。这是因为 time.Second 常量是一个 Duration 类型的的常量,在决定算术结果的类型的时候, time.Second 常量的类型被提升了。为了使得 Add 方法能够成功调用,这个常量还要进一步地从 Duartion 类型的 常量 隐式转换成为 Duartion 类型的 变量

如果常量没有了现在这些特性的话,我们上面的各种赋值与函数调用都要显式地转换一下。看看如果我们尝试用一个 int 型的值去做相应的方法调用:

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 函数调用的参数的时候,我们会收到一个编译错误。编译器不会允许有类型的变量进行隐式的类型转换。要想上面的代码编译通过的话,我们需要执行一个显式的类型转换:

Add(time.Duration(difference))

常量是唯一不用我们执行显式类型转换的机制。

结论

我们对常量的行为习以为常,这恰恰正好就是语言的设计者和实现这个特性的人辛勤劳动的最好证明。为了让常量能够像现在这样方便,并且希望它能为使用者带来便利,工作人员投入了很多心血和努力。

所以下一次你在用一个常量的时候,记住你现在使用的是一个特殊的,隐藏在编译器里面的瑰宝,尽管它并没有被人广泛的发掘出来。常量让 Go 编程更有乐趣、可读性更强以及更直观,并且同时还能保证我们写的代码是类型安全的。

感谢

感谢 Nate FinchKim Shrier 为我的文章进行了多次的校对,这让我的文章的内容和示例更加准确,流畅并且更加有趣。我有很多次都打算放弃了,是 Nate 的鼓励使我继续写下去。

特别要感谢 Robert Griesemer 和 Go 开发团队的开发者付出它们的时间和耐心指导我一些细节的问题。Go 开发团队的成员都非常的厉害,并且对整个社区还有社区的成员都非常关心。由衷感谢!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员的数学

程序员的数学

结城浩 / 管杰 / 人民邮电出版社 / 2012-10 / 49.00元

如果数学不好,是否可以成为一名程序员呢?答案是肯定的。 本书最适合:数学糟糕但又想学习编程的你。 没有晦涩的公式,只有好玩的数学题。 帮你掌握编程所需的“数学思维”。 日文版已重印14次! 编程的基础是计算机科学,而计算机科学的基础是数学。因此,学习数学有助于巩固编程的基础,写出更健壮的程序。 本书面向程序员介绍了编程中常用的数学知识,借以培养初级程序员的数学思维。读......一起来看看 《程序员的数学》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具