深入学习Go语言
上QQ阅读APP看书,第一时间看更新

2.2 变量

2.2.1 变量以及声明

Go语言中有四类标记:标识符(identifiers)、关键字(keywords)、运算符(operators)标点符号(punctuation)以及字面量(literals)。

Go语言变量标识符由字母、数字、下画线组成,其中首字符不能为数字,同一字母的大小写在Go语言中代表不同标识。

根据Go语言规范,标识符命名程序实体,例如变量和类型。标识符是一个或多个Unicode字母和数字的序列。标识符中的第一个字符必须是Unicode字母。

Go语言规范中,下画线“_”也被认为是字母。

在Unicode标准8.0中,第4.5节“常规类别”定义了一组字符类别。Go语言将Unicode中任何字母类别Lu、Ll、Lt、Lm或Lo中的所有字符视为Unicode字母,将数字类别Nd中的字符视为Unicode数字。

据统计,Go语言视为Unicode的字母(含下画线“_”)一共有20871个(包括汉字),见表2-5。

表2-5 Unicode字母表

在Go语言中,命名标识符时,通常选择英文的52个大小写字母以及数字0~9和下画线来组合成合适的标识符。表2-5中其他的字符也可以用于标识符,但不在表中的字符是不能用在Go语言标识符中的。大写字母主要是指Lu类别中的1781个字母。

另外,Go语言中关键字是保留字,不能作为变量标识符,见表2-6。

表2-6 Go语言关键字表

Go语言变量声明使用关键字var,下面声明了几个变量:

上面这种写法一般用于声明多个全局变量,通常在函数外定义。单个变量的声明不用加圆括号,比如:

记住,这些变量在Go语言中都会自动赋予对应的零值,但零值并不一定是0,后面有关于零值的详细说明。

多个变量可以在同一行进行赋值,也称为并行或同时或平行赋值。如:

简式声明(short variable declarations),也叫短变量声明,它是具有初始化表达式的常规变量声明的简写,但没有类型(隐式类型定义,会根据其使用环境而推断出它所具备的类型)。如:

上面代码中表达式右边的值以相同的顺序赋值给表达式左边的变量,所以a的值是5,b的值是7,c的值是 "abc"。

简式声明可能常常出现在函数内部。但在某些上下文中,例如“if”,“for”或“switch”语句的初始化程序中也可以使用,它们可用于声明本地临时变量。

简式声明虽然一般用在函数内,但要注意的是:全局变量和简式声明的变量尽量不要同名,否则很容易产生偶然的变量隐藏(Accidental Variable Shadowing)。

即使对于经验丰富的Go开发者,这也是一个常见的陷阱,很难发现。例如:

程序输出:

上面代码的输出一方面验证了变量隐藏这个现象,另一方面也展现了Go语言中的作用域问题。其实所谓变量隐藏就是因为变量作用域不同导致的现象。在一个作用域中,声明一个标识符并使用时,需要注意它的使用范围即作用域。

下面是有关Go语言中作用域的规则:

■ 在Go语言中,在顶层声明的常量、类型、变量或函数的标识符的范围是包块。

■ 导入包名称范围是包含导入声明的文件的文件块。

■ 方法接收器、函数参数或结果变量的标识符的范围是函数体。

■ 在函数内声明的常量或变量标识符的范围从声明语句的末尾开始,到最内层包含块的末尾结束。

■ 在函数内声明的类型标识符的范围从标识符开始,到最内层包含块的末尾结束。

■ 块中声明的标识符可以在内部块中重新声明。

如果想要交换两个变量的值,则可以简单地使用:

在Go语言中,这样省去了使用交换函数的过程。

空白标识符_也被用于抛弃值。如:

_实际上是一个只写变量,不能得到它的值。这样做是因为Go语言中必须使用所有被声明的变量,但有时并不需要使用从一个函数得到的所有返回值。

Go语言有个强制规定,在函数内一定要使用全部声明的变量,若存在未使用的变量,则代码将编译失败。因此可以将该未使用的变量改为空白标识符_或者干脆注释掉。但未使用的全局变量是没问题的,没有这个限制。

上面代码中全局变量x未使用不会影响程序的编译,但如果去掉变量y后面的注释符,则编译不通过,因为变量y没有使用,这个规则在Go语言中是强制性的。

另外,在Go语言中,如果导入的包未使用,就不能通过编译。如果不直接使用包里的函数,而只是调用包中的init()函数,或者调试代码时去掉了对某些包的功能使用,可以添加一个下画线标识符“_”来作为这个包的名字,从而避免编译失败。在import语句中,下画线标识符用于表示导入,但不会在包中使用。例如:

上面代码中,fmt包就没有使用,所以使用空白标识符。

并行赋值也用于当一个函数返回多个返回值时,比如下面的val和错误err是通过调用Func1()函数同时得到的:

对于布尔值,好的命名能够很好地提升代码的可读性,例如以is或者Is开头的isSorted、isFinished、isVisible,使用这样的命名能够在阅读代码时,获得阅读正常语句一样的良好体验,例如标准库中的unicode.IsDigit(ch)。

在Go语言中,指针属于引用类型,其他的引用类型还包括切片、字典和通道,如果传递引用类型参数或者赋值给引用类型变量,原始数据有改动时它们也会发生变化。

注意,Go语言中的数组是值类型,因此向函数中传递数组时,函数会得到原始数组数据的一份副本。如果打算更新数组的数据,可以考虑使用数组指针类型。

被引用的变量一般存储在堆内存中,以便系统进行垃圾回收(GC),且比栈拥有更大的内存空间。但Go编译器会自动做出选择,程序员不能直接判断其在内存中的位置,究竟是堆内存还是栈内存。

执行go tool compile命令,可以看到上面代码中 &x发生了逃逸,x被储存在堆中。

Go编译器会做逃逸分析,所以由Go的编译器决定在哪里(堆或栈)分配内存,保证程序的正确性。

2.2.2 零值(nil)

当一个变量被var声明之后,如果没有为其明确指定初始值,Go语言会自动初始化其值为此类型对应的零值,见表2-7。

表2-7 零值表

对其他类型的零值,特别是复合类型的零值,Go语言会自动递归地将每一个元素初始化为其类型对应的零值。

nil表示pointer、interface、function、map、slice和channel的零值。如果代码中指定变量的类型,编译器将无法编译代码,因为它猜不出具体的类型。

在一个nil的切片中添加元素是没问题的,但需要注意的是对一个字典做同样的事将会生成一个运行时的异常。

字符串不会为nil,这对于经常使用nil分配字符串变量的开发者而言是个需要注意的地方。

根据前面的介绍,其实这样写和上面的效果一样: