Go语言圣经 第3章 基础数据类型

197 阅读5分钟

3. 基础数据类型

虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整数、浮点数、比特数组、内存地址等。Go语言提供了丰富的数据组织形式,这依赖于Go语言内置的数据类型。这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构的便捷性。

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。

  • 基础类型包括数字、字符串和布尔型等。
  • 复合数据类型,包括数组和结构体,是通过组合简单类型,来表达更加复杂的数据结构。
  • 引用类型包括指针、切片、字典、函数、通道,虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。
  • 接口类型是一种特殊的空类型,Golang中所有类型都实现了接口类型。

Golang内置以下基础数据类型:

类型名称长度零值说明
bool布尔1false
byte字节10
rune40
int,uint4或80
int8,uint810
int16,uint1620
int32,uint3240
int64,uint6480
float3240.0
float6480.0
complex648
complex12816
uintptr4或8足以存储一个指针的uint3或uint64整数
string""UTF-8字符串

3.1. 布尔

一个布尔类型的值只有两种:true和false。if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。一元操作符!对应逻辑非操作,因此!true的值为false

布尔值可以和&&(AND)和||(OR)操作符结合,并且有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值。因此,下面的表达式总是安全的:

s != "" && s[0] == 'x'

其中s[0]操作如果应用于空字符串将会导致panic异常。

3.2. 整型

Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,与此对应的是uint8、uint16、uint32和uint64四种无符号整数类型。

这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint;其中int是应用最广泛的数值类型。这两种类型都有同样的大小,32或64bit,但是我们不能对此做任何的假设;因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。

type rune int32

字节byte类型也是uint8类型的等价类型。byte类型一般用于强调数值是一个原始的数据,而不是一个小的整数。

type byte uint8

最后,还有一种无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

不管它们的具体大小,int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是不同的类型,即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然。

其中有符号整数采用2的补码形式表示,也就是最高bit位用来表示符号位,一个n-bit的有符号数的值域是从-2n-1到2n-1-1。无符号整数的所有bit位都用于表示非负数,值域是0到2n-1。例如,int8类型整数的值域是从-128到127,而uint8类型整数的值域是从0到255。

3.3. 浮点数

Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。

这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38;对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。

一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大(译注:因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差):

var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1)    // "true"!

浮点数的字面值可以直接写小数部分。小数点前面或后面的数字都可能被省略(例如.707或1.)。很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分。

用Printf函数的%g参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用%e(带指数)或%f的形式打印可能更合适。

math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;还有NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1)。

3.4. 复数

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0。

在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部),我们可以用自然的方式书写复数,就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化:

x := 1 + 2i
y := 3 + 4i

复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。

3.5 字符和字符串

一般来说说,字符就是一个字符,而字符串是一个连续的字符序列。在Go语言中,声明一个字符变量使用单引号包围一个有效的字符,而声明一个字符串变量使用双引号包围一个连续的字符序列。

c := 'c'
fmt.Printf("%T\n", c) // int32
s := "string"
fmt.Printf("%T\n", c) // string

需要注意的是,字符的类型是int32,而字符串的类型是string。

在Go语言中,一个字符串是采用UTF8编码的rune序列(rune本质上是int32类型,用来表示一个Unicode码点)。字符串可以包含任意的数据,包括byte值0,但通常是人类可读的文本。

对于一个字符串,内置的len函数可以返回字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。

s := "hello, world"
fmt.Println(len(s))     // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')

c := s[len(s)] // panic: index out of range

字符串支持切片操作。子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串,新字符串将包含j-i个字节。如果索引超出字符串范围或者j小于i的话将导致panic异常。另外,不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置。

fmt.Println(s[0:5])        // "hello"
fmt.Println(s[0:len(s)])   // "hello, world"
fmt.Println(s[0:len(s)+1]) // panic: index out of range

fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:])  // "hello, world"

字符串支持+操作符,可以把两个字符串连接构造一个新字符串。

字符串可以用==<进行比较:逐个字节比较。因此。字符串比较的结果是字符串自然编码的顺序。

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。

不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。

3.5.1. 字符串字面值

字符串一般用字面值方式编写,只要将一系列可见的字符包含在双引号内即可:

s := "hello, world"

另外,字符串字面值也可以包含Unicode码点。比如,以反斜杠``开头的转义序列在字符串字面值中插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:

\a      响铃
\b      退格
\f      换页
\n      换行
\r      回车
\t      制表符
\v      垂直制表符
'      单引号(只用在 ''' 形式的rune符号面值中)
"      双引号(只用在 "..." 形式的字符串面值中)
\      反斜杠

举例:

s := "hello,\nworld"
fmt.Println(s)

Go语言可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是\xhh,其中两个h表示十六进制数字(大写或小写都可以)。一个八进制转义形式是\ooo,包含三个八进制的o数字(0到7),但是不能超过\377(对应一个字节的范围,十进制为255)。

举例:

s := "hello, world\x21"
fmt.Println(s) // hello, world!
s2 := "hello, world\041"
fmt.Println(s2) // hello, world!

字符串字面值还有一种特殊形式:原生字符串,是使用反引号...包围的文本字符串序列。在原生字符串中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行。

举例,

s := `hello,\nworld`
fmt.Println(s)

注意:在原生字符串内部无法直接写```字符,需要用八进制或十六进制转义或+"`"连接字符串常量完成。

原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。

3.5.2. Unicode

在很久以前,计算机世界只有一个字符集:美国信息交换标准代码ASCII。ASCII字符集使用7bit来表示128个字符:包含英文字母的大小写、数字、各种标点符号和设备控制符。

随着互联网的发展,混合多种语言的数据变得很常见(比如中文、日文等多种语言字符)。如何有效处理这些包含了各种语言的丰富多样的文本数据呢?答案就是使用Unicode( unicode.org ),它收集了这个世界上所有的符号系统,包括各种不同的符号以及文字,每个符号都分配一个唯一的Unicode码点。在第八版的Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言。

Unicode码点(code point),即一个字符在Unicode编码中的编号。每个编号占四个字节,32个bit。

在Go语言中,一个字符的Unicode码点可以用rune类型(底层类型是int32)来表示。所以,一个字符串可以认为是Unicode码点的序列。

例如,一个字符串可以转为一个rune类型(或int32)的Unicode码点序列。

s := "hello, world"
r := []rune(s)
fmt.Println(r) // [104 101 108 108 111 44 32 119 111 114 108 100]

s2 := "hello, 世界"
r2 := []int32(s2)
fmt.Println(r2) // [104 101 108 108 111 44 32 19990 30028]

这种编码格式叫UTF-32或UCS-4,即每个字符的Unicode码点都使用同样大小的32bit来表示。

注意,Go语言没有使用这种编码方式。这种编码方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,而每个ASCII字符本来只需要8bit或1字节就能表示。

3.5.3. UTF-8

UTF8是一个将Unicode码点编码为字节序列的变长编码格式。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。

UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110/1110/11110,则说明需要2/3/4个字节;后续的每个高端bit都以10开头。

0xxxxxxx                             runes 0-127    (ASCII)
110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)

变长的编码无法直接通过索引来访问第n个字符(因为字符串的索引操作返回字节值),但是UTF8编码获得了很多额外的优点。

  • UTF8编码比较紧凑,完全兼容ASCII码;效率高,通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。
  • UTF8是前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看。没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。
  • UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列。
  • UTF8没有嵌入的NUL(0)字节,可以很好地兼容那些使用NUL作为字符串结尾的编程语言。

Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数(比如区分字母和数字、字母的大小写转换等),unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。

有很多Unicode字符很难直接从键盘输入,有一些甚至是不可见的字符。在Go语言中,可以使用转义字符和Unicode码点在字符串中包含任意的字符(通常是用于特殊字符)。转义字符有三种形式:\xhh对应小于128的码点值(即ASCII表中的字符),\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字。

例如,字符串A的Unicode转译格式。

a := "A"
a2 := "\x41"
a3 := "\u0041"
a4 := "\U00000041"

s := "世"
s2 := "\u4e16"
s3 := "\U00004e16"
s4 := "\xe4\xb8\x96" // "世"占3个字节,每个字节的十六进制表示

另外,Unicode转义也可以使用在rune字符中。

例如,字符A的Unicode转译格式。

a := 'A'
a2 := '\x41' // \x00 - \xff
a3 := '\u0041'
a4 := '\U00000041'

s := '世'
s2 := '\u4e16'
s3 := '\U00004e16' 
// s4 := '\xe4\xb8\x96' // not allowed

对于一个字符,如果其码点值大于256(一个字节可以表示的范围为0-255),则必须使用\u\U转义形式。因此,\xe4\xb8\x96并不是一个合法的rune字符,虽然这三个字节对应着“世“的Unicode码点。

得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀:

func HasPrefix(s, prefix string) bool {
    return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

或者是后缀测试:

func HasSuffix(s, suffix string) bool {
    return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

或者是包含子串测试:

func Contains(s, substr string) bool {
    for i := 0; i < len(s); i++ {
        if HasPrefix(s[i:], substr) {
            return true
        }
    }
    return false
}

对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。

如果我们真的关心每个Unicode字符,我们可以使用其它处理方式。举例,对于字符串Hello, 世界,它包含了中西两种一共9个Unicode字符。

图3.5 range循环解码UTF-8字符串

图3.5展示了它的内存表示形式。因为Go语言使用UTF8编码格式,所以该字符串的存储为13个字节。

import "unicode/utf8"

s := "Hello, 世界"
fmt.Println(len(s))                    // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"

为了处理这些真实的字符,我们需要一个UTF8解码器。unicode/utf8包提供了该功能,我们可以这样使用:

for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%d\t%c\n", i, r)
    i += size
}

// output
0  H
1  e
2  l
3  l
4  o
5  ,
6   
710

每一次调用DecodeRuneInString函数都返回一个r和长度,r对应字符本身,长度对应r采用UTF8编码后的编码字节数目。长度用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的,我们需要更简洁的语法。

幸运的是,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。需要注意的是,对于非ASCII,索引更新的步长将超过1个字节。

for i, r := range "Hello, 世界" {
    fmt.Printf("%d\t%q\t%d\n", i, r, r)
}

// output
0  'H'  72
1  'e'  101
2  'l'  108
3  'l'  108
4  'o'  111
5  ','  44
6  ' '  32
7  '世'  19990
10  '界'  30028

文本字符串采用UTF8编码只是一种惯例,但是对于循环的真正字符串并不是一个惯例。如果用于循环的字符串只是一个普通的二进制数据,或者是含有错误编码的UTF8数据,将会发生什么呢?

每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符\uFFFD,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。

UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。比如,将[]rune类型转换应用到UTF8编码的字符串,将返回字符串编码的Unicode码点序列:

s := "world"
fmt.Printf("% x\n", s) // 77 6f 72 6c 64
r := []rune(s)
fmt.Printf("%x\n", r) // [77 6f 72 6c 64]

s2 := "世界"
fmt.Printf("% x\n", s2) // e4 b8 96 e7 95 8c
r2 := []rune(s2)
fmt.Printf("%x\n", r2) // [4e16 754c]

如果将一个[]rune类型的Unicode字符slice或数组转为string,则对它们进行UTF8编码:

fmt.Println(string(r))  // world
fmt.Println(string(r2)) // 世界

如果将一个整数转为字符串,则是生成只包含对应Unicode码点字符的UTF8字符串:

fmt.Println(string(65))     // "A", not "65"
fmt.Println(string(0x4eac)) // "京" 

如果对应码点的字符是无效的,则用\uFFFD无效字符作为替换:

fmt.Println(string(1234567)) // "?"

3.5.4. 字符串和byte切片

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。

strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。

在Go语言中,一个字符串是采用UTF8编码的rune序列,其底层是是一个包含只读字节的数组。所以字符串一旦创建,是不可变的。

字符串和字节slice之间可以相互转换。例如,

s := "abc"
b := []byte(s)
s2 := string(b)

从概念上讲,[]byte(string)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据,但总的来说需要确保在新的字节数组(例如变量b)被修改的情况下,原始的字符串也不会改变。string([]byte)转换是根据一个字节slice构造一个字符串拷贝,以确保构造出的字符串是只读的。

为了避免转换中不必要的内存分配,bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数:

func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string

bytes包中也对应的六个函数:

func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string

它们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数。

bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的。

举例,

var buf bytes.Buffer
buf.WriteByte('1')
buf.WriteByte('2')
buf.WriteByte('3')
fmt.Println(buf)

当向bytes.Buffer添加任意字符的UTF8编码时,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。

3.5.5. 字符串和数字的转换

除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。

将一个整数转为字符串,一种方法是用fmt.Sprintf返回一个格式化的字符串;另一个方法是用strconv包中Itoa函数:

x := 123
y := fmt.Sprintf("%d", x) // "123"
y2 := strconv.Itoa(x)     // "123"

strconv包中的FormatInt和FormatUint函数可以用不同的进制来格式化数字:

fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"

fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多,特别是在需要包含有附加额外信息的时候:

s := fmt.Sprintf("x=%b", x) // "x=1111011" 

如果要将一个字符串解析为整数,可以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数:

x, err := strconv.Atoi("123")             // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits 

ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。

有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。

3.6. 常量

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。

一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。例如,常量比变量更适合用于表达像π之类的数学常数,因为它们的值不会发生变化:

const pi = 3.14159 // approximately; math.Pi is a better approximation

所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof。

一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型。在下面的代码中,time.Duration是一个命名类型,底层类型是int64,time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型,可以通过%T参数打印类型信息:

const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay)     // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout)     // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"

3.6.1. iota 常量生成器

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

周日将对应0,周一为1,如此等等。

我们也可以在复杂的常量表达式中使用iota。下面是一个更复杂的例子,每个常量都是1024的幂:

const (
    _ = 1 << (10 * iota)
    KiB // 1024
    MiB // 1048576
    GiB // 1073741824
    TiB // 1099511627776             (exceeds 1 << 32)
    PiB // 1125899906842624
    EiB // 1152921504606846976
    ZiB // 1180591620717411303424    (exceeds 1 << 64)
    YiB // 1208925819614629174706176
)

不过iota常量生成规则也有其局限性。例如,它并不能用于产生1000的幂(KB、MB等),因为Go语言并没有计算幂的运算符。

3.6.2. 无类型常量

Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的):

fmt.Println(YiB/ZiB) // "1024"

另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:

var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换。

对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。

只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。

var f float64 = 3 + 0i // untyped complex -> float64
f = 2                  // untyped integer -> float64
f = 1e123              // untyped floating-point -> float64
f = 'a'                // untyped rune -> float64

上面的语句相当于:

var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')

无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。

对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:

i := 0      // untyped integer;        implicit int(0)
r := '\000' // untyped rune;           implicit rune('\000')
f := 0.0    // untyped floating-point; implicit float64(0.0)
c := 0i     // untyped complex;        implicit complex128(0i)

注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。因为如果不知道浮点数类型的内存大小是很难写出正确的数值算法的。

fmt.Printf("%T\n", 0)        // "int"
fmt.Printf("%T\n", 0.0)      // "float64"
fmt.Printf("%T\n", 0i)       // "complex128"
fmt.Printf("%T\n", '\u0000') // "int32" (rune)

当尝试将这些无类型的常量转为一个接口值时,这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。