深入浅出Go语言核心编程
上QQ阅读APP看书,第一时间看更新

2.1 变量

变量在使用时往往分为声明和赋值两个步骤。其中声明除了给定占位符外,更重要的是要指定数据类型。因为在应用程序执行时,变量都要经历内存分配,而分配的内存大小主要取决于数据类型。

2.1.1 变量声明

在Go语言中,可以利用var关键字来声明一个变量,例如:

    var length int

var是variable的缩写形式,表示这是一个变量声明;length为变量名;int则为变量的数据类型。对于多个相同数据类型的变量,也可以进行一次性声明。例如,同时声明两个int类型的变量,其名称分别为length和size,对应的代码如下:

    var length, size int

对于多个变量,如果它们的数据类型不同,则需要分别进行声明,例如:

    var length, size int
    var success bool

除此之外,还可以共用var关键字,以上变量声明可以写作如下形式。

    var (
    length, size int
    success bool
    )

而在Java等编程语言中,则直接省略了这一关键字。例如,在Java中声明一个变量,使用如下语法:

    int length;

一向崇尚简单的Go语言,破天荒地增加了var来表示变量,在变量声明方面比Java多出一个关键字。这是因为Go语言为常量单独保留了一个关键字const,var和const分别对应于变量和常量的声明。

2.1.2 变量赋值

变量在声明后,可以利用“=”来对变量进行赋值操作。例如:

    var length int
    length = 10
1.多重赋值

对于多个变量,可以利用如下语句同时进行赋值:

    var length, size int
    length, size = 10, 20

此时,变量length和size分别被赋值为10和20。在一行代码中为多个变量一起赋值,这就是我们期盼已久的多重赋值。我们自然也会想到,是否可以利用多重复赋值语句来交换两个变量的值呢?

代码清单2-1演示了变量a和b的值进行互换的常规做法(不考虑数据溢出的极端情况)。

代码清单2-1 在不引入第三个变量的情况下,交换两个变量的值

运行该代码段,在控制台上的输出如下:

    原始值:a= 10 , b= 20
    执行后:a= 20 , b= 10

可见成功实现了a, b值的互换。在实现该功能之余,我们对于完成如此简单的功能却需要3条语句,多少会感到有些沮丧。而在Go语言中,可以直接利用多重赋值实现。对应的代码如代码清单2-2所示。

代码清单2-2 利用多重赋值实现两值互换

在上面的代码段中,我们直接利用如下代码完成了a、b值的交换。

    a, b = b, a

不只是数值型的互换,利用多重赋值还可以实现其他数据类型的互换,当然,前提是两个变量有着相同的数据类型。

2.变量的零值

在Go语言中,如果一个变量只进行了声明而没有对它赋值,其实也会依据数据类型赋予变量零值。以下是各种数据类型对应的零值。

(1)整型的零值为0。

(2)浮点型的零值为0.0。

(3)布尔型的零值为false。

(4)字符串类型的零值为空字符串("")。

(5)数组类型的零值是按元素的数据类型将其所有元素置为零值。

(6)接口类型(interface)、切片(slice)、通道(channel)、字典(map)、指针(pointer)、函数(function),它们的零值均为空(nil)。

(7)结构体(struct)的零值是对其字段/元素填充零值。

这与大多数编程语言的规则一致,比较特殊的是结构体和数组。通过二者的填充规则我们可以看出,Go语言总是尽量避免真正意义上的空值。如此一来,也间接地减少了类似Java/C++中的空指针异常带来的困扰。

2.1.3 同时进行变量声明和赋值

我们可以在变量声明的同时对它赋值,例如:

    var a int = 10

在IDE工具GoLand中,该代码行中的“int”会呈现浅灰色。将光标置于int关键字上,GoLand会给出优化提示——类型可以忽略,如图2-1所示。

图2-1 GoLand中忽略类型关键字的提示

在提示窗口中单击“More actions...”,会给出修正操作建议。最直接的操作建议是删掉int关键字。我们直接删掉int关键字,在没有指定数据类型的情况下仍可以正常编译执行,这是因为Go语言的另外一项功能——类型推断。

类型推断是指,如果变量声明和赋值在同一条语句中完成,那么编译器可以从具体值中推断出数据类型,而不需要显式声明数据类型。例如:

    var a int = 10

可以简写为:

    var a = 10

更进一步,利用操作符“:=”可以对该语句在形式上进行简化。

    a := 10

需要注意的是,这3种写法都是正确的写法,实现的功能也完全相同,只是代码形式不同而已。当然,采用哪种写法完全依赖程序员的个人风格和团队的代码规范。

类型推断也与运行平台息息相关。例如,对于整数,往往会被推断为int;对于浮点数,往往会被推断为float64(64位平台)。如果类型推断的结果并不是程序员的本意,那么显式指定变量的数据类型仍然是必要的。

在上面的示例中,var a int = 10中的int是冗余的类型声明。如果要求变量a的类型为uint,则必须显式指定数据类型,如下所示。

    var a uint = 10

因为uint与类型推断获得的结果(int)不同,所以该语句在GoLand等IDE中不会出现优化提示。

2.1.4 多重赋值与“:=”操作符

操作符“:=”表示同时进行变量声明和赋值,因此,对一个变量进行多次“:=”操作,将会出现编译错误,例如:

    a := 10
    a := 20

对变量a的第二次赋值需要使用“=”进行赋值,而不是“:=”,因为不能对同一个变量进行两次声明。正确的代码如下:

    a := 10
    a = 20

另外,我们可以利用一条语句来对两个变量同时进行赋值。如果其中一个已经声明过,而另外一个未声明,那么应该使用“:=”还是“=”呢?

这种混合情况,应该使用“:=”。Go语言会兼容处理这种情况。即使变量a已经声明过,但变量b并未声明过,因此利用“:=”进行多重声明和赋值也是合法的,如图2-2所示的代码不会出现编译错误。

图2-2 多重赋值与“:=”的使用

2.1.5 没有多余的局部变量

Go语言中的局部变量有一个很独特的特点——没有一个变量是多余的。这意味着我们不能声明一个局部变量却在整个程序中不使用它。

例如,声明了一个变量a并为它赋值10,但在后续代码中,变量a未被真正使用到。此时编译执行该代码,将会出现“未使用的变量a(Unused variable 'a')”的错误提示,如图2-3所示。

图2-3 声明但并未使用局部变量,将会出现编译错误

这一规则保证了代码的尽量整洁。Java开发利器Intellij IDEA和GoLand属于同一IDE家族。对于Java程序中出现的声明但未使用的局部变量,Intellij同样可以识别,但只会在变量处给出优化提示(如同图2-1中的浅灰色样式)。可见,声明但不使用变量,是与编程整洁性相悖的,Go语言对这一点执行得更加严格。

2.1.6 全局变量

对于习惯了面向对象编程的程序员来说,一提到变量,潜意识里总是将它识别为局部变量,这可能是因为面向对象编程中很少提到全局变量的概念。但在Go语言中,全局变量(variable)是和函数处于同一地位的成员。全局变量的作用域为整个.go文件中的任意位置,同时,也可以被其他.go文件导入并引用。代码清单2-3演示了全局变量的定义和使用。

代码清单2-3 全局变量的定义和使用

在上面的代码中,声明了一个全局变量a,变量a在整个.go文件的所有函数中均可访问。

2.1.7 全局变量与链接

除了作用域外,我们从另外一个角度来看看全局变量与局部变量的区别:仅声明,不使用。

在图2-4中,声明了一个全局变量a,并将它赋值为10,但没有任何一处代码使用到该变量。当编译时,不会出现编译错误。

图2-4 声明但并未使用全局变量,不会出现编译错误

同样是变量,为什么未使用局部变量会出现编译错误,而全局变量不会呢?甚至在类似GoLand这样的强大的IDE工具中,连优化、警告一类的提示都没有。

这是因为编译器(Compiler)不能识别出全局变量是否真的不会被引用。因为全局变量意味着可以被外部引用,而非限于当前.go文件(在第18章会看到在汇编文件中引用.go文件中的全局变量的例子)。那么全局变量到底在哪些地方被引用呢?其实是在链接阶段,由链接器(Linker)完成引用。而链接阶段处于编译阶段之后,因此,编译器无法识别全局变量的引用情况,也就不会出现编译错误。