Alan Hou的个人博客

云原生系列Go语言篇-基础类型和变量声明

本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。

经过前面的学习,我们已配置好了环境,下面就要学习Go语言的特性以及如何最好地使用。在探寻到“最好”的方式之前,有一条首要原则:按照清晰表达意图的方式编写代码。在讲解这些特性的过程中,我们会看来哪些选择以及为什么某个方法能产生更整洁的代码。

我们会先学习基础类型和变量。虽然每个程序员都有这些概念,但Go在有些方面并不相同,并且Go和其它语言有一些细微的差别。

内置类型

Go和其它语言一样有一些内置类型:布尔类型、整型、浮点型以及字符串类型。地道地使用这些类型对于从其它语言转过来的开发人员有时会存在挑战。我们会讲到这些类型以及如何在Go中最好地进行使用。在学习这些类型之前,我们先讲解一些适用所有类型的概念。

零值

Go和大部分现代语言一样,对于声明没赋值的变量会默认赋零值。显式的零值可以让代码更整洁并且去除了C和C++程序中存在的bug。在讲到每种类型时,都会顺带提到这些类型的零值。

字面量

Go中的字面量表示所书写的数字、字符或字符串。在Go程序中有四类常见的字面量(在讨论复数时会讲到第五种不常见的字面量)。

整型字面量是一组数字,通常是十进制的,但可以使用不同的前缀来表示其它前缀:0b为二进制,0o为八进制,0x为十六进制。前缀可以使用大写字母或小写字面。前面加0后面不接任何字母是另一种表示八进制字面量的方式。但不推荐使用,会产生误解。

为更易于阅读更长的整型字面量,Go允许我们在字面量中间添加下划线。比如这样我们可以对十进制数字进行千分位分组(1_234)。这些下划线对数值没有任何影响。唯一的限制是下划线不能放在数字的开头和结尾,下划线也不能连着下划线。可以在字面量的每一位之间放下划线(1_2_3_4),但不推荐这么干。对十进制数字按千分位分隔,或是二进制、八进制及十六进制按一字节、两字节或4字节分隔来提升可读性。

浮点数字面量包含有小数点用于表示小数位。也可以使用字母e加正数或负数来表示指数(如6.03e23)。也可以使用0x来写成十六进制以及字母p来表示指数。类似于整型字面量,可以使用下划线来格式化浮点数字面量。

符文(Rune)字面量以单引号包围字符进行表示。和很多其它语言不同,Go语言中的单引号与双引号不通用。符文字面量可以写成单个Unicode字符('a')、8位八进制数字('\141')、8位十六进制数字('\x61')、16位十六进制数字('\u0061')或32位Unicode数字('\U00000061')。还有一些反斜线转义符文字符,最有用的有换行('\n')、制表符('\t')、单引号('\'')、双引号('\"')和反斜线('\\')。

实践中对符文字面量使用十进制来表示数字,除非使用十六进制转义能让代码更整洁,否则请避免使用。八进制的使用很少见,大多数用于表示POSIX权限标记(比如对rwxrwxrwx使用0o777)。有时使用十六进制和二进制来用于比特位过滤或网络和基础架构应用。

有两种表示字符串字面量的方式。大部分时候应使用解释字符串字面量(比如输入"Greetings and Salutations")。它们包含允许形式的零个或多个符文字面量。无法使用的字符有未转义的反斜杠、换行和双引号。如果使用解释字符串字面量并希望两个问候在不同行上,并希望Salutations放在双引号中,需要输入"Greetings and\n\"Salutations\""

如需在字符串使用反斜杠、换行和双引号,还可以使用原生字符串字面量。使用反引号(`)作为限界符,其中可包含反引号之外的任意字符。在使用原生字符串字面量时,多行问候可这么写:

显示类型转化一节中会学习到如果所声明的内存大小不一样的话即使是两个整型变量也无法相加。但Go允许我们在浮点表达式中使用整型字面量,甚至是对浮点型变量赋整型字面量。这是因为Go中的字面量无类型,可配合任意与该字面量兼容的变量使用。我们在类型、方法和接口一章中会看到甚至可以对字面量使用基于基础类型的自定义类型。无类型只能如此,无法将字符串字面量赋值给数字类型变量或是将数字字面量赋值给字符串变量,也无法将浮点字面量赋值给整型。这些编译都识别为错误。

字面量无类型是因为Go是一种务实的语言。在开发者指定类型前不强制任何类型是站得住脚的。存在内存大小限制,虽然可以向整型写入其所能存储的大数,但在所赋字面量导致具体变量溢出时,会出现编译时错误,比如给类型为byte的变量赋字面量1000。

在变量赋值一节中会学到,Go语言中存在一些场景不显式声明类型。这时,Go使用字面量的默认类型;如果表达式中不能清晰表明字面量类型的话,字面量会默认使用一种类型。我们会在学习各种内置类型时讲到字面量的默认类型。

布尔类型

bool类型表示布尔变量。bool类型的变量可为这两个值之一:truefalsebool的零值为false

很难没进行变量声明时讨论变量类型,反之亦然。我们会先使用变量声明,在var对照:=一节中进行描述。

数字类型

Go有很多种数字类型:三大类共十二个类型(以及一些特殊名称)。如果读者是从JavaScript这种仅有一个数字类型的语言转过来的,可能会觉得太多了。但实际上只有几种类型是常用的,其它是较少用到。我们会讨论整型,之后再转到浮点型以及极少使用的复数类型。

整型

Go提供了不同类型大小的有符号和无符号整型,从1字节到8字节大。请参见下表:

表2-1 Go语言中的整型

类型名数值范围
int8–128 to 127
int16–32768 to 32767
int32–2147483648 to 2147483647
int64–9223372036854775808 to 9223372036854775807
uint80 to 255
uint160 to 65535
uint320 to 4294967295
uint640 to 18446744073709551615

名称应该很明确了,所有整型的零值都是0

特殊整型

Go中整型有一些特殊名称。byteuint8的别名,对byteuint8进行赋值、比较或是执行数学运算都是合法操作。但在Go代码中很少会看到uint8 的使用,直接称其为byte

第二个特殊名称是int。在32位CPU中,int为32位有符号整型,同int32。在大多烽64位CPU中,int都是64位有符号整型,同int64。因为int在不同平台上不一致,在int

int32int64之前未进行类型转换(参见显示类型转换一节)的赋值、比较和数学运算都会报编译时错误。整数字面量默认为int类型。

:有一些不常见的64位CPU架构对int类型使用32位的无符号整型。Go语言支持的有三种:amd64p32、mips64p32和mips64p32le。

第三个特殊名称是uint。它与int遵循相同的规则,只是它是无符号的(值总是为0或正值)。

整型还有两个特殊名称:rune 和uintptr。此前我们已学习到符文字面量,并且在品品字符串和符文类型一节中会讨论到rune类型,在第16章中会讲到uintptr

选择使用哪个整型

Go语言中的整数类型比一些其它语言要多。有这么多种选择,你可能会想在什么时候使用哪个。有三个简单的原则:

注:很可能会在老代码中看到一些同样功能的函数,只是一个参数与变量是int64 而另一个是uint64。这是因为这些API是在Go中加入泛型之前编写的。没有泛型,我们需要需要对不同类型使用稍有不同的名称编写相同的算法。使用int64uint64 意味着只需要编写一次代码,调用者可通过类型转换进行传递并转换所返回的值。

可以在Go标准库中看到很多这种用法,在strconv 包中有FormatInt/FormatUintParseInt/ParseUint

整数运算符

Go整型支持常见算术运算:+、 - 、* 、/以及取余的%。整数相除得到的仍是整数,如果希望得到浮点值,需要使用类型转换将整数转换为浮点数。同时要当时将整数除以0,这会导致panic(我们会在第9章中详细讨论panic)。

注:Go语言中整数相除遵循向下取值的规则,参见Go文档中的算术运算符一节。

可以将任意一个算术运算符与=合并用于修改变量:+=-=*= 、/=%=。例如,执行如下代码,最终x的值为20.

可使用==!=>>=<<=对整数进行对比。

Go中还有对整数操作的位运算法。可通过<<>>整行左移和右侧,或是使用& (逻辑与)、| (逻辑或)、^ (逻辑异或)和&^ 逻辑与非)进行位掩码运算。和算术运算符一样,我们可以将逻辑运算符与= 进行拼接修改变量:&=|=^=&^=<<=>>=

浮点类型

Go语言中有两种浮点类型,见下表:

表2-2 Go语言中的两种浮点类型

类型名最大绝对值最小(非零)绝对值
float323.40282346638528859811704183484516925440e+381.401298464324817070923729583289916131280e-45
float641.797693134862315708145274237317043567981e+3084.940656458412465441765687928682213723651e-324

和整型一样,浮点类型的零值为0.

Go语言中的浮点类型类似于其它语言中的浮点类型。Go使用IEEE 754规范,提供了很大范围和有限的精度。选择用哪种浮点类型非常直接:无需兼容已有格式的话,使用float64。浮点字面量默认类型为float64,因而保持选用float64是最简单的选择。这还有助于消除浮点精度问题,因为float32只有6位或7位小数精度。除非通过性能测试发现产生了较大问题(第15章中会讲解测试和性能调优),否则大可不必担心内存占用上的差别。

更大的问题是到底是否该使用浮点数。大多数情况下的答案都是否定的。和其它语言一样,Go的浮点数范围很广,但无法存储这个范围内的所有值,存储的是最近似值。因为浮点是不精确的,只能用到可接受不精确值或是充分理解浮点值规则的场景。这就将其限制在图形及科学计算领域。

警告:浮点数无法精确表达小数。不要在表示金钱或其它要求精确值的地方使用它。我们会在第10章中处理精确小数值时学习一个第三方模块。

IEEE 754

前面已经提到,Go(以及大部分其它编程语言)都是按照IEEE 754规范存储浮点数。

具体规则不在本文讨论范围内,不是太容易理解。例如,如果以float64存储–3.1415,其在内存的64位存储表现为:

其值为 –3.14150000000000018118839761883。

很多程序员都知道整数的二进制是什么样的(最右侧的位为1,紧接着是2,再接着是4,依此类推)。浮点数完全不同。在64位之中,1位用于表示符号位(正数或负数),11位用于表示二进制指数(也称阶码),还有52位用于以归一化的格式表示数字(也称为尾数)。

点击这里更深入地了解IEEE 754。

对浮点数可以使用所有的数学和比较运算符,%除外。浮点数的除法有一些有趣的属性。非零浮点变量除以0根据符号位的不同会返回+Inf-Inf(正无穷或负无穷)。而值设为0的浮点变量除以0时会返回NaN (Not a Number)。

虽然Go语言允许使用==!=进行浮点比较,但别这么做。因为浮点数不精确的天然属性,两个读者自认为相等的数字可以不相等。应当定义一个最大允许误差,然后比较两个浮点的差值是否比它小。这个误差(有时称为epsilon)取决于所需的精度,没有放之四海而皆准的规则。如果读者不清楚,可以向有经验的人寻求帮助。如果身边没有这样的人,可以点击这里进行了解(这更说明了仅在绝对必要时才使用浮点数)。

复数类型(大概率用不到)

还有一种数字类型,不太常见。Go语言对复数提供一级支持。如果读者不知道复数是什么,可以直接跳过这部分。

Go语言中对复数的支持并不算多。Go中定义了两个复数类型。complex64 使用float32 值来表示实部和虚部,complex128 使用float64 值。两者都在complex 内置函数中声明。Go通过一些规则决定函数输出的类型:

复数可以使用所有的标准算术运算符。和浮点值一样,可以使用==!=进行复数的比较,但存在精度问题,最好使用误差来比较。可以通过内置函数realimag 来分别提取复数的实部和虚部。在math/cmplx包中还有一些函数可用于操作complex128 值。

复数两种类型的零值是将0赋给实部和虚部。

例2-1中为一个简单函数演示了如何使用复数。读者可以自行在The Go Playground中运行。

例2-1 复数

运行以上代码的得到的结果如下:

通过这里的输出也可以看出浮点是不精确的。

如果你在想第5种基础类型字面量是什么,Go支持通过虚部字面量来表示复数的虚部。它和浮点字面量很像,只是在结尾多了个i

虽然内置了复数类型,但在数据科学领域并不算流行。主要是因为语言中没有支持其它的一些特性(如矩阵),各种库需要使用效率较低的替代,如切片的切片(我们会在第3章中学到切片并在第6章学习如何实现切片)。但如果需要在程序中运算Mandelbrot集合或是实现二次方程式,都可以使用到复数的支持。

读者可能会想Go语言为什么为内置复数。答案很简单:Go语言的创始人之一,Ken Thompson(他同时也是Unix的发明人之一),觉得它们很有趣(你永远GET不到一个大佬的笑点~)。有一些要在Go的后续版本中删除复数的讨论,但直接忽略这个特性会简单。

注:如果希望使用Go编写科学计算的应用,可以使用一个第三方包Gonum。它用到了复数并提供了线性代数、矩阵、积分和数据统计的库。但建议首先考虑其它语言。

品品字符串和符文类型

接下来就是字符串了。和大部分现代语言一样,Go内置了字符串类型。字符串的零值是一个空字符串。Go支持Unicode:我们在字符串字面量一节中已经展示过,可以把Unicode字符放到字符串中。类似整型和浮点类型,字符吕可使用==进行相等的比较、使用!=进行不等的比较,或是使用 >>=<<=进行排序。通过+运算符可进行字符串拼接。

Go中的字符串是不可变的,可以对字符串变量重新赋值,但无法赋给它的字符串本身的值。

Go还有表示单个代码点的类型。符文类型是int32类型的别名,就像byteuint8的别名一样。不难猜到,符文字面量的默认类型是rune,而字符串字面量的默认类型是string。

如果是针对字符,请使用rune类型,而不是int32。在编译器中它们可能没分别,但我们应该通过类型声明代码的意图。

在下一章中我们会更详细地讨论字符串,讲解其实现细节、与字节和符文类型的关联以及其高级特性和缺点。

显式类型转换

很多编程语言中的数字类型可按需自动相互转换。这称为自动类型提升,虽然看起来很方便,但结果是制定规则合理地将一种类型转换成另一种类型很复杂并常常会产生意料外的后果。Go是一种重视清晰意图和可读性的语言,它不允许变量间的自动类型提升。对于类型不一致的变量必要使用类型转换。哪怕是内存大小不同的整数和浮点数都必须转换为相同的类型再进行运算。这样类型非常清楚,不必去记住任何类型转换规则(参见例2-2)。

例2-2

以上代码中我们定义了4个变量。xint类型,值为10,y为float64类型,值为30.2。因为它们不是同一类型,需要先转换类型再相加。对于变量z,我们使用float64类型转换将x转换为float64,对于变量d,我们使用int类型转换x转换为int。运行代码,输出结果为40.2 40。

这种对类型的严格限制还有其它作用。因为在Go中所有的类型转换都是显式的,也就无法将其它Go类型看成布尔类型。在很多编程语言中,非零数字或非空字符串都被解释成布尔值true。和自动类型提升一样,不同语言对真值规则的不一致也会产生困扰。Go不允许这种真值也在意料之中。事实上其它类型不论隐式还是显式都无法转换成布尔类型。如果想将其它数据类型转换为布尔类型,必须使用比较运算符 (==!=><<=, or >=)。比如,要检测变量x是否为0,相应的代码为x == 0。而对字符串s判空则使用s == ""

注:类型转换算是Go中使用篇幅换取简单和清晰性的一处。我们会多次看到这类权衡。地道的Go将易理解性放在简洁性之上。

var对照:=

Go作为一种很克制的语言其有声明变量的方式不可谓不多(Rob Pike曾在公开演讲中表示如果重新设计Go语言的话他不会保留这么多种变量声明方式)。原因在于:每种声明样式与样式的使用方式有关。我们来看下在Go中有哪些声明变量的方式,以及它们适用的场景。

在Go语言中声明变量最冗长的一种方式是使用var关键词、显式类型及所赋值。如下:

如果=右侧为所需要的变量类型,可以省掉=左侧的类型。因为整型字面量的默认类型为int,以下语句会将变量x声明为int类型:

反过来,如果想声明变量赋零值,可以保留类型而去除=及右边内容:

通过var可一次声明多个变量,类型可相同:

相同类型的零值:

或是类型不同:

var还有一种用法。如果一次声明多个变量,可放到一个声明列表中:

Go还支持短声明。在函数内时,可使用:=运算符来代替使用类型推断的var 声明。如下两行功能相同,都是声明值为10的int类型:

类似于var,可以使用:=一次声明多个变量。下面两行都是将10赋值给x、hello赋值给y

:=可完成一样var无法完成的功能:它还可以赋值给已有的变量。只要:=左侧存在一个新变量,就可对其它已有的变量赋值:

:=存在一个限制。如果是在包级别声明变量,必须使用var:=无法在函数外使用。

如何决定使用哪种方式呢?一如既往,选择让意图最清晰的那个。函数内最常用的声明方式是:=。函数外,在声明多个包级变量时使用声明列表。

有如下场景应避免在函数内使用:=

虽然var:=都允许在同一行中声明多个变量,但建议仅在函数或逗号ok语句返回多个值时使用它(见第3章和第5章)。

应尽少在函数外声明变量,这称为包代码块(见第4章)。修改包级变量不是好的做法。变量位于函数外时,很难追踪对其所做的修改,这会让了解程序中数据的流动变得困难。也可能带来不易发现的bug。通常仅应在包代码块中声明效果上不可变的变量。

小贴士:避免在函数外声明变量,因为这会让数据流的分析变更复杂。

你可能会想Go是否提供了保证数据不可变的方式呢?是的,但它与其它编程语言略有不同。下面就该学习const了。

使用const

很多语言都有声明不可变值的方式。在Go中通过const关键字实现。乍看起来和其它语言完全一样看啊。请在The Go Playground中测试例2-3的代码。

例2-3 const声明

运行以上代码会出现报以下错误的编译错误:

可以看到,我们在包级别以及函数内部声明了变量。和var一样,可以(也应该)在括号内声明一组关联的常量。

但注意Go语言中的const是非常受限的。Go中的常量用于为字面量提供名称。仅能存储编译器在编译时可解析的值。这表示可赋以下值:

注:下一篇文章中我们会讲到lencap。还有一个值可配合const使用,那就是iota。我们会在第7章中讲解创建自有类型时谈到iota

Go并没有提供方式指定运行时计算值为不可变。我们会在下一篇文章中学到,不存在不可变的数组、切片、字典(map)或是结构体,也无法在结构体中声明字段不可变。看到这里感觉也没什么限制嘛。在函数内部,变量是否被修改非常清楚,所有可变性并没有那么重要。在第5章中,我们会学习到Go是如何防止传递给函数的参数发生修改的。

小贴士:Go中的常量是一种为字面量提供名称的方式。无法在Go中声明一个变量为不可变。

有类型和无类型常量

常量可以是有类型的,也可以是无类型的。无类型和字面量一样,自身没有类型,在无法推断出其它类型时具有一个默认类型。有类型变量仅能赋值给该类型的变量。

常量是否有类型取决于为何声明这个常量。如果是给定数学常量一个名称用于多种数值类型,那么应保持为无类型。通常,保留常量为无类型具有更强的灵活性。有些场景下会希望对常量添加类型。在第7章通过iota创建枚举时会使用到有类型常量。

无类型的常量声明如下:

以下声明都是合法的:

有类型常量的声明如下:

该常量仅能赋值给int类型变量。将其赋值给其它类型会报编译时错误:

未使用变量

Go的一个目标是让大团队的编程协助更简单。为此,Go设定了一些独特的规则。在云原生系列Go语言篇-Go环境配置中,我们学习了Go程序需要通过go fmt来进行格式化,让其更容易编写代码操作工具以及提供代码规范。Go的另一个要求是每个本地变量都需要被读取。声明了本地变量却未读取会报编译时错误。

编译的变量未使用检测并不极端。只要变量有过一次读取,编译器就不会报错,即使写入的变量再未被读取过也没问题。可在The Go Playground中运行如下有效代码:

虽然编译器和go vet无法捕获到对x的未使用的10和30的赋值,有一些第三方工具可进行这一检测。在第11章中会讨论到这些工具。

注:Go编译器无法阻止你创建未读取的包级变量。这是另一个避免创建包级变量的原因。

未使用常量

可能有些出乎意料,Go编译器允许我们通过const创建未读取的常量。这是因为Go语言中的常量在编译时进行计算,不会产生负面效果。这样删除起来也很容易,如果常量未使用,只需要不在编译后的二进制文件中包含它即可。

变量和常量的命名

Go中变量命名规则和Go开发者对变量和常量命名遵循的规范不是一回事。和大部分语言一样,Go要求标识名称以及字母或下划线开头,名称中可包含数字、下划线和字母。Go对于字母和数字的定义比其它语言要更宽泛一些。任意可看成字母或数字的Unicode字符都支持。因此在例2-4中的变量定义在Go中完全有效。

例2-4 不应使用的变量名

虽然以上代码可以使用,但请不要使用这类变量命名。这些名称不地道的原因是它破坏了熟知代码功能的基本规则。这些名称不易理解或是在键盘上不易输入。长相近似的Unicode危害最大,因为虽然看起是同一个字符,却完全是不同的变量。可以在The Go Playground中运行例2-5的代码。

例2-5 使用相近码点的变量名

运行程序得到的结果如下:

虽然下划线可用于变量名,但很少使用,因为地道的Go不使用蛇形命名(如index_counter或number_tries)。而是在出现多个单词时使用驼峰命名(indexCounter 或numberTries)。

注:下划线本身 (_)在Go中是特殊标识符名称,我们会在第5章中讲解函数时讲到它。

在很多编程语言中,常量使用全大写字母表示,单词之间使用下划线分隔(如INDEX_COUNTER或NUMBER_TRIES)。Go并不遵循这一规范。这是因为Go靠包级声明的第一个字母决定其是否可在包外访问。我们会在第10章中讲到包时再来看这个知识点。

函数内更多使用短变量名。变量的作用域越小,所用名称越短。Go中单字母变量很常见。例如,使用k 和v (key和value的缩写)作为for-range循环中的变量名。如果使用标准的for 循环,常使用ij作为索引的变量名称。常用类型变量命名还有一些其它的地道方式,在讲到标准库时会不断提到。

有些编程语言使用弱类型体系,鼓励开发人员在变量名中包含类型名。而Go是强类型语言,无需这样去记录变量类型。但对于变量类型和单字母名称还有一些规范。人们会使用类型的第一个字母作为变量名(如整型用i,浮点型用f、布尔型用b)。在定义自有类型时,规范也类似。

这些短名称有两个目的。每一是它们消除了重复的类型名,让代码更简短。第二是它用于检测代码的复杂度。如果发现无法搞清短名称变量是什么,很有可能是代码块过长了。

在包级对变量和常量命名时,使用更具描述性的名称。名称中还是不应包含类型,但因其作用域更广,需要使用更完整的名称来清晰体现其所表示的值。

小结

本文讲解到很多内容,我们掌握了如何使用内置类型、声明变量以及使用赋值语句和运算符。在下一篇文章中,我们会学习Go中的复合类型:数组、切片、字典和结构体。我们还会再回到字符串和符文,了解它们是如何使用字符编码的。

退出移动版