Go语言学习指南:惯例模式与编程实践
上QQ阅读APP看书,第一时间看更新

2.1 内置类型

与其他语言一样,Go语言内置了很多基础类型:布尔型(boolean)、整型(integer)、浮点型(float)和字符串(string)。哪怕有经验的开发者,初学Go语言时也不一定能娴熟地使用这些类型。我们先介绍一些基本概念。

2.1.1 零值

与大多数现代高级编程语言一样,在Go语言中会给声明但未赋值的变量默认赋零值,这样的显式地赋零值的方式不仅使代码更简洁清晰,还消除了一些在C和C++中存在的问题。在接下来的基础类型中,我们将学习它们各自的零值。

2.1.2 字面量

在Go语言中一个值的字面形式称为一个字面量,一个值可能有很多种字面量形式,它可能是数字(整数和浮点数)、字符或者字符串(第5种字面量将在讨论复数类型的时候进行学习)。

整数字面量默认情况下是十进制的数字序列,但是使用不同的特定前缀(不区分大小写)来表示不同进制的数。例如,当以0b或0B为前缀时表示二进制(以2为基数),以0o或0O为前缀时表示八进制(以8为基数),以0x或0X为前缀时表示十六进制(以16为基数)。定义整数字面量时以0为前缀也可以表示八进制,但是请尽量不要使用这种容易让人误解的方式。

为了使较长的整数字面量更容易理解,Go语言可以将_作为字面量中的间隔来增强可读性,且没有任何副作用,例如,对于十进制数,我们可以按千分位为间隔,1_234就等同于1234。只要_不在整型字面量的开头和结尾,且不是多个_彼此相连,则都是正确的。尽管你可以将1234写成1_2_3_4,这在语法上虽然正确,但不建议使用。对于整型字面量,建议在千分位分隔十进制,或者在1字节、2字节或4字节的地方分隔二进制、八进制以及十六进制[1]

浮点数字面量由整数部分、小数点、小数部分组成。也可以表示成指数形式,如6.03e23。当表示十六进制数时,可以使用0x作为前缀和字母p来表示指数。与整数字面量一样,也可以通过_分隔较长的浮点数字面量[2]

字符字面量由被一对单引号包围的字符组成,注意,在Go语言中单引号与双引号表示的是不同字面量。字符字面量可以写成Unicode字符('a')、8位八进制数('\141')、8位十六进制数('\x61')、16位十六进制数('\u0061')或32位Unicode数('\U00000061')。反斜线还可以结合一些字符表示转义字符,常见的有换行符('\n')、制表符('\t')、单引号('\'')、双引号('\"')、和反斜杠('\\')。

实际使用中,我们最好使用十进制来表示数字字面量,并尽量避免使用十六进制转义来表示字符字面量,除非需要更清晰的表达意图。八进制几乎很少出现,主要用于表示POSIX文件系统中的权限值,比如可以使用0o777来表示rwxrwxrwx。当基础设施或者网络通信中运行的程序需要对数据量进行过滤时,将会使用到十六进制或者二进制的字符字面量。

字符串字面量有两种格式:原始字符串字面量(raw string literal,反引号风格)和解释型字符串字面量(interpreted string literal,双引号风格)。例如,字符串"Greetings and Salutations"使用双引号风格。字符串字面量可以正常显示除未转义的反斜杠、换行符和双引号以外的任何字符。如果你需要“Greetings and”和“Salutations”不在一行显示,并且Salutations用双引号显示,那么可以使用"Greetings and\n\"Salutations\""

当你希望字符串中包含反斜杠、双引号或者换行符时,可以使用原始字符串字面量。原始字符串字面量用反引号(`)分隔,并且可以包含除反引号之外的任意字符,示例如下所示:

正如我们将在2.1.6节中看到的,如果两个整数变量声明为不同的大小,那么它们不能相加。不过,Go允许你在浮点表达式中使用整数字面量,甚至可以将整数字面量赋值给浮点变量。这是因为Go中的字面量是无类型的,因此这些字面量可以使用或操作任何与之兼容的变量,在第7章中介绍自定义类型时,我们甚至可以看到可以对基于基础类型的自定义类型使用字面量。也由于字面量是无类型的,所以不能将字符串字面量赋值给数值变量,不能将数值字面量赋值给字符串变量,也不能将浮点数字面量赋值给整型变量,否则就会产生编译错误。

Go语言从实用主义考虑,为了避免字面量指定不正确的类型,字面量被设计为类型不确定的。虽然可以定义一个超过整型长度限制的数值字面量,但是赋值给整型变量时会产生溢出编译错误。比如,如果我们将1000的字面量赋值给byte类型,就会产生溢出编译错误。

我们将在关于变量赋值的内容中看到一些不需要显式声明类型的情况,在这些情况下,Go语言使用字面量的默认类型。如果在表达式中没有明确指定字面量的类型,就使用默认类型,在后文中,我们将继续讨论哪些内置类型可以用于字面量的默认类型。

2.1.3 布尔型

我们用bool类型表示布尔变量,bool变量只有两种状态值:falsetruebool类型的零值是false

由于变量类型无法脱离变量声明分开讨论(它们是变量的一个整体),所以我们将在2.2节进行详细解释。

2.1.4 数值类型

Go语言内置庞大的数值类型体系,可分为3大类共12种不同的数值类型。如果你有其他语言的编程经验,比如JavaScript中仅包含一个数值类型,相比而言Go的数值类型就太多了。实际编程中,一些类型经常被使用,另一些则过于复杂而较少使用,本节我们将依次学习整型、浮点型和复数型(极少使用)。

整型

Go中有带符号整型和无符号整型,由1~4个字节组成[我们使用字节(byte)作为值大小的度量单位。一个字节相当于8个比特,所以uint32类型的大小为4字节,即每个uint32值占用4个字节],见表2-1。

表:2-1

这些整型的零值都是0

特殊的整型

Go语言有一些整型的名称十分特殊。byteuint8的别名,所以可以在byteuint8之间进行赋值、比较或者执行数学运算。更多的时候,在代码中使用byte更加直观,很少使用uint8

第二个特殊的是int,在32位CPU上,int等同于int32;在64位CPU上,int等同于int64。因为int在不同平台中的差异性,所以在不做显式类型转换(参见2.1.6节)的情况下,在intint32或者int64之间进行赋值、比较或者执行数学运算时会产生编译时错误。整数字面量默认使用int类型。

 在一些特殊的64位CPU上,int等同于int32。Go语言支持其中的三种:amd64p32、mips64p32和mips64p32le。

第三个特殊的是unit,它遵循与int一样的语言规范,但是因为uint是无符号整型,所以uint的值只能是0或者合法取值范围以内的正数。

最后两个特殊的整型是rune(参见2.1.5节)和uintptr(参见第14章)。

正确使用整型

Go针对很多不同的场景提供了多种整型,当对整型的使用存在疑惑时,Go官方推荐了三条规则作为参考:

  • 在处理二进制文件或者实现网络协议时,请根据实际需要的变量大小和有无符号选择对应的整型。
  • 如果正在编写一个可适用于任何整型的库函数,通常推荐编写一对函数,一个函数的参数和变量为int64,另一个为uint64(详见第5章)。

 一般分别使用int64uint64作为参数,因为Go语言目前既不支持泛型(尽管已经发布Go泛型的预览版,但是并未正式发布完整的泛型特性),也不支持函数重载。如果没有这些语言特性,当需要编写作用和算法相同但参数不同(int64uint64)的函数时,函数名称必须略有不同。这些函数通过类型转换来处理不同的参数。

这样的模式可以在Go标准库中大量使用。例如strconv包中的FormatInt/FormatUint函数对和ParseInt/ParseUint函数对。这样,算法和逻辑相同,但是函数参数类型不同,可以为不同类型编写单独的函数,在math/bits的包中大量使用了这种模式。

  • 其他情况下,int可以通用。

 除非需要集成其他系统或者有特殊的性能要求,需要显式说明整型大小或者有无符号,否则使用int即可。不必做过度设计和优化。

整型操作符

Go支持常用的算术运算符:+、-、*、/,以及取模(%)。整型的除法运算结果还是整型,如果希望得到浮点型的结果,就需要使用类型转换将整型转换成浮点型。另外,请注意除数不能为0,否则会导致程序异常,详见8.8节。

 整型除法的小数点后的数会被丢弃(这叫作“向零截断的原则”)。Go语言规范有完整详细的算术运算符介绍,请访问链接https://oreil.ly/zp3OJ

这些算术运算符可以与等号(=)结合,用于简化整型的运算,例如+=、-=、*=、/=和%=,它们会修改变量的值,例如,下面代码中x的最终值是20:

除此以外,Go语言也可以在整数中使用位运算符:<<(左移)、>>(右移)、&(与)、|(或)、^(异或)、&^(与非)。与算术运算符一样,也可以将它们与等号(=)结合,简化整型的逻辑运算:&=、|=、^=、&^=、<<=和>>=。

浮点型

Go支持两种浮点型:float32float64,参见表2-2。

表2-2:Go语言的浮点型

浮点型的零值也是0

Go和其他编程语言都有浮点数运算,Go语言的浮点型也遵循IEEE 754规范。float64相较float32有更高的精度,建议在没有特定兼容性要求的情况下,尽量使用float64。浮点数字面量的默认类型也是float64,所以float64是最简单的选择。使用float64可以表示精度更高的浮点数,且不用担心float64占用更多内存,除非你使用分析器(详见第13章)诊断后确定它是导致问题的原因。

当计算中需要精确的数字时,我们需要了解浮点数的一些不足,这样才能更好地使用它。因为存储的长度,浮点数只能取到近似值,丢失了部分精度,这在图形学和科学计算中尤为突出。

 浮点数不能精确地表示十进制数。任何对精度要求比较高的情况下都不要使用浮点数,比如表示金额。

IEEE 754

Go语言遵循IEEE 754规范。但是这些规范既复杂又超出了本书的学习范围,所以只在这里做一点简单介绍。比如,当使用float64存储-3.1415这个浮点数时,64位的内存存储形式为:

这个值实际等于–3.14150000000000018118839761883。

我们都知道整数的二进制表示方式(1表示1,10表示2,100表示4,以此类推)。但是浮点数的二进制表示方式完全不同,在64位的二进制中,1位用于表示有无符号(正或负),紧接着的11位用于表示二进制数的指数,剩下的52位用于表示二进制数的分数(也叫作尾数)。

更详细的规范内容可以前往维基百科查看(https://oreil.ly/Gc05u)。

可以对浮点数进行除了%以外的任意数学运算或者比较运算。另外,浮点数除法有一些有趣的特性,任何非0的数除以浮点数0,将返回+Inf(也叫正无穷大)或者-Inf(也叫负无穷大),其中正或者负符号取决于被除数的符号。当0除以0时得到NaN(“Not a Number”的简写)。

同样出于浮点型不精确的特点,最好不要对浮点型使用==和!=比较运算符,你可能会发现比较两个浮点数时结果和预期不一致,这是因为计算机在处理浮点数时会丢失精度。通常比较两个浮点数是否相等需要定义一个最小方差,然后比较两个浮点数之间差异是否小于这个最小值(有时也称为epsilon),它取决于比较时对精度的要求[3]。如果需要更透彻的比较方法,需要经过数学和数值分析过程,由于不在本书的学习范围中,这里不做讨论。

复数型

复数型通常不会被使用,尽管Go语言对复数有一流的支持。但是如果你对复数型不太关心,可以跳过本节。

Go语言支持两种复数型:complex64complex128complex64复数值的实部和虚部都是float32类型的值。complex128复数值的实部和虚部都是float64类型的值。complex64complex128都可以使用内置函数complex来声明。在使用函数时有以下4个规则:

  • 当使用无类型常量或字面量作为参数时,创建的complex也是无类型的,其默认类型是complex128
  • 当两个参数都是float32类型时,函数将返回complex64类型的值。
  • 任何一个参数是float32类型时,函数将返回complex64类型的值。
  • 其他情况下函数都将返回complex128类型的值。

所有的标准算术运算符都适用于复数。并且与浮点数一样,虽然可以使用==和!=来进行比较复数,但是同样会遇到精度问题,所以最好也使用epsilon的方式解决。复数在Go中原生内置realimag两个函数,分别用来提取复数的实部和虚部。在math/cmplx包中还有一些其他函数也可以操作complex128类型复数的值。

复数的实部和虚部的零值均为0

示例2-1是一段简单的代码,主要演示复数的工作原理。如果你有兴趣,可以在The Go Playground(https://oreil.ly/fuyIu)中运行代码。

示例2-1:复数

以上代码的运行结果如下所示:

这里你可以发现浮点数是不精确的。

前面我们学习了4种字面量,Go语言支持的第5种字面量就是虚数字面量,用来表示复数的虚部。它们虽然看起来像浮点数字面量,但其实虚部都是由一个i作为后缀的。

尽管Go语言内置了复数型,我们可以使用Go提供的复数计算方程、处理曼德布洛特集合(Mandelbrot set),但是毕竟Go的设计目标不是一个数学计算和科学计算的编程语言,所以对一些数学计算编程特性提供了替代方案,比如当需要矩阵时,可以用二维的切片(slice)代替(关于切片的学习详见第3章和第6章)。

大多数情况下是不会使用复数的,所以复数的存在一直饱受争议。最早的Go语言缔造者之一(同样也是UNIX的缔造者)Ken Thompson认为它们会很有趣(https://oreil.ly/eBmkq),但是有人(https://oreil.ly/Q76EV)曾建议在未来的版本中移除复数。

 如果需要编写数学计算的应用程序,建议你使用其他更合适的编程语言。如果确实要选择Go语言,则推荐使用第三方库 Gonum(https://www.gonum.org/)。它拥有很多复数的高级特性,并且为线性代数、矩阵、积分和统计等提供了库函数。

2.1.5 字符串和字符

字符串是现代高级编程语言中的必备类型,Go语言内置了字符串类型,字符串的零值为空字符串。Go语言支持Unicode编码[4],在2.1.2节中我们了解到任何Unicode编码的字符都可以放入字符串中,并且与整型、浮点型一样,字符串可以使用==和!=进行比较,使用>、>=、<或者<=进行比较排序,使用+将字符串合并成一个新的字符串。

字符串是不可变的,编程中这表示一个一旦被设置就不能改变的值,即当我们修改了一个字符串的变量的值后,字符串其实已经不是原来的字符串了。

字符串中的每一个Unicode字符被称为字符(rune),而且字符是一种特殊的类型,它其实是int32的别名,就像前面介绍的byteuint8的别名一样[5]。字符字面量的默认类型是字符,字符串字面量的默认类型是字符串。

 尽管runeint32的别名,但是编程场景中需要使用字符时,建议使用rune,这样可以增加代码的可读性,明确表达程序的逻辑。

在下一章中,我们将深入地探讨字符与字符串在Go语言中的实现与细节,并展示字节与字符之间的关系,以及高级特性和陷阱。

2.1.6 显式类型转换

在一部分编程语言中数字类型可以自动转换成别的类型,这称为“自动类型转换”,看起来似乎非常便利,但是这样常常会让代码变得复杂而难以理解,甚至出现一些难以预料的错误。Go语言的设计从清晰表达意图和可读性出发,不允许变量之间进行自动类型转换,当变量之间类型不一致时,就必须进行显式类型转换。即使是整型或者浮点型,只要类型的大小不同,也必须转换成相同的类型。这样的设计使变量及其使用更清晰,不需要你牢记一系列晦涩的类型转换规则。参见示例2-2。

示例2-2:类型转换

这里我们定义了4个变量,x是一个值为10的整型变量(int),y是一个值为30.2的浮点型变量(float64)。由于它们的类型不同,因此需要转换后才能将它们进行相加,计算结果z是一个浮点型(float64),所以需要将x转换为float64类型。计算结果d是一个整型(int),所以需要将float64类型的y转换成int类型。当运行这段代码,得到的z值是40.2,d值是40。

严格的类型还有其他意义,由于在Go语言中所有类型转换都必须是显式的,所以不能将一些类型看作布尔类型。在一些编程语言中,一个非0数字或者非空字符串都解释成布尔值true,这是上面提到的自动类型转换,这样的“真值”(true)行为在Go中不被支持,无论是使用显式还是隐式的类型转换都不行,只能通过比较运算符(==、!=、>、<、<=或者>=)。例如,如果需要判断x是否等于0,则使用x==0;如果需要判断字符串s是不是空字符串,则使用s==""

 Go语言认为代码通俗易懂优于简短,为了程序的简单性和清晰性,Go语言选择在类型转换时增加一些冗余代码,这样的权衡取舍在Go语言中还有很多。


[1]例如,0600、0_600和0o600是合法的,42_和4__2是不合法的。

[2]例如,72.40、1E6和0X.8p-0是合法的,1.5e1_、0x1.5e-2和1._5是不合法的。

[3]用if(abs(f1-f2)<epsilon)来对比较结果进行判断,一般epsilon取1E-6。

[4]Go的源文本也是UTF-8编码。

[5]Unicode的字符长度是4个字节。