常量和变量
这篇文章将介绍常量和变量相关的知识。 类型不确定值、类型推断和值的显式类型转换等概念也将被介绍。
上一章中提到的基本类型的字面量表示 (除了false
和true
)都属于无名常量(unnamed constant),或者叫字面常量(literal constant)。 false
和true
是预声明的两个有名常量。本文将介绍如何声明自定义的有名常量。
在Go中,有些值的类型是不确定的。换句话说,有些值的类型有很多可能性。 这些值称为类型不确定值。对于大多数类型不确定值来说,它们各自都有一个默认类型, 除了预声明的nil
。nil
是没有默认类型的。 我们在后续的文章中将了解到很多关于nil
的知识。
与类型不确定值相对应的概念称为类型确定值。
上一章提到的字面常量(无名常量)都属于类型不确定值。 事实上,Go中大多数的类型不确定值都属于字面常量和本文即将介绍的有名常量。 少数类型不确定值包括刚提到的nil
和以后会逐步接触到的某些操作的布尔返回值。
一个字面(常)量的默认类型取决于它为何种字面量形式:
- 一个字符串字面量的默认类型是预声明的
string
类型。 - 一个布尔字面量的默认类型是预声明的
bool
类型。 - 一个整数型字面量的默认类型是预声明的
int
类型。 - 一个rune字面量的默认类型是预声明的
rune
(亦即int32
)类型。 - 一个浮点数字面量的默认类型是预声明的
float64
类型。 - 如果一个字面量含有虚部字面量,则此字面量的默认类型是预声明的
complex128
类型。
类型不确定常量的显式类型转换
和很多语言一样,Go也支持类型转换。 一个显式类型转换的形式为T(v)
,其表示将一个值v
转换为类型T
。 编译器将T(v)
的转换结果视为一个类型为T
的类型确定值。 当然,对于一个特定的类型T
,T(v)
并非对任意的值v
都合法。
下面介绍的规则同时适用于上一章介绍的字面常量和即将介绍的类型不确定有名常量。
对于一个类型不确定常量值v
,有两种情形显式转换T(v)
是合法的:
v
T
类型的一个值。 转换结果为一个类型为T
的类型确定常量值。v
的默认类型是一个整数类型(int
或者rune
) 并且T
是一个字符串类型。 转换T(v)
将v
看作是一个Unicode码点。 转换结果为一个类型为T
的字符串常量。 此字符串常量只包含一个Unicode码点,并且可以看作是此Unicode码点的UTF-8表示形式。 对于不在合法的Unicode码点取值范围内的整数v
, 转换结果等同于字符串字面量"\uFFFD"
(亦即"\xef\xbf\xbd"
)。0xFFFD
是Unicode标准中的(非法码点的)替换字符值。 (但是请注意,今后的Go版本可能只允许rune或者byte整数被转换为字符串。 从Go官方工具链1.15版本开始,go vet
命令会对从非rune和非byte整数到字符串的转换做出警告。)
事实上,第二种情形并不要求v
必须是一个常量。 如果v
是一个常量,则转换结果也是一个常量。 如果v
不是一个常量,则转换结果也不是一个常量。
一些合法的转换例子:
下面是一些非法的转换:
int(1.23) // 1.23不能被表示为int类型值。
uint8(-1) // -1不能被表示为uint8类型值。
float64(1+2i) // 1+2i不能被表示为float64类型值。
// -1e+1000不能被表示为float64类型值。不允许溢出。
float64(-1e1000)
// 0x10000000000000000做为int值将溢出。
int(0x10000000000000000)
// 字面量65.0的默认类型是float64(不是一个整数类型)。
string(65.0)
// 66+0i的默认类型是complex128(不是一个整数类型)。
string(66+0i)
从上面的例子可以看出,一个类型不确定数字值所表示的值可能溢出它的默认类型的表示范围。 比如上例中的-1e1000
和0x10000000000000000
。 一个溢出了它的默认类型的表示范围的类型不确定数字值是不能被转换到它的默认类型的(将编译报错)。
注意,有时一个显式转换形式必须被写成(T)(v)
以免发生歧义。 这种情况多发生在T
不为一个标识符的时候。
我们以后将在其它章节学到更多的显式类型转换规则。
类型推断介绍
Go支持类型推断(type deduction or type inference)。 类型推断是指在某些场合下,程序员可以在代码中使用一些类型不确定值, 编译器会自动推断出这些类型不确定值在特定情景下应被视为某些特定类型的值。
在Go代码中,如果某处需要一个特定类型的值并且一个类型不确定值可以表示为此特定类型的值, 则此类型不确定值可以使用在此处。Go编译器将此类型不确定值视为此特定类型的类型确定值。 这种情形常常出现在运算符运算、函数调用和赋值语句中。
有些场景对某些类型不确定值并没有特定的类型要求。在这种情况下,Go编译器将这些类型不确定值视为它们各自的默认类型的类型确定值。
上述两条类型推断规则可以被视为隐式转换规则。
本文下面的章节将展示一些类型推断的例子。 后续其它文章将会展示更多类型推断的例子和规则。
和无名字面常量一样,有名常量也必须都是布尔、数字或者字符串值。 在Go中,关键字const
用来声明有名常量。 下面是一些常量声明的例子。
package main
// 声明了两个单独的有名常量。(是的,
// 非ASCII字符可以用做标识符。)
const π = 3.1416
const Pi = π // 等价于:const Pi = 3.1416
// 声明了一组有名常量。
const (
No = !Yes
Yes = true
MaxDegrees = 360
Unit = "弧度"
)
func main() {
// 声明了三个局部有名常量。
const DoublePi, HalfPi, Unit2 = π * 2, π * 0.5, "度"
}
Go白皮书把上面每行含有一个等号=
的语句称为一个常量描述(constant specification)。 每个const
关键字对应一个常量声明。一个常量声明中可以有若干个常量描述。 上面的例子中含有4个常量声明。除了第3个,其它的常量声明中都各自只有一个常量描述。 第3个常量声明中有4个常量描述。
在上面的例子中,符号*
是一个乘法运算符, 符号!
是一个布尔取否运算符。 运算符将在中详述。
常量声明中的等号=
表示“绑定”而非“赋值”。 每个常量描述将一个或多个字面量绑定到各自对应的有名常量上。 或者说,每个有名常量其实代表着一个字面常量。
在上面的例子中,有名常量π
和Pi
都绑定到(或者说代表着)字面常量3.1416
。 这两个有名常量可以在程序代码中被多次使用,从而有效避免了字面常量3.1416
在代码中出现在多处。 如果字面常量3.1416
在代码中出现在多处, 当我们以后欲将3.1416
改为3.14
的时候,所有出现在代码中的3.1416
都得逐个修改。 有了有名常量的帮助,我们只需修改对应常量描述中的3.1416
即可。 这是常量声明的主要作用。当然常量声明也可常常增加代码的可读性(代码即注释)。
以后,我们使用非常量这一术语表示不是常量的值。 下一节将要介绍的变量就属于非常量。
注意,常量可以直接声明在包中,也可以声明在函数体中。 声明在函数体中的常量称为局部常量(local constant),直接声明在包中的常量称为包级常量(package-level constant)。 包级常量也常常被称为全局常量。
包级常量声明中的常量描述的顺序并不重要。比如在上面的例子中, 常量描述No
和Yes
的顺序可以掉换一下。
上面例子中声明的所有常量都是类型不确定的。 它们各自的默认类型和它们各自代表的字面量的默认类型是一样的。
类型确定有名常量
我们可以在声明一些常量的时候指定这些常量的确切类型。 这样声明的常量称为类型确定有名常量。 在下面这个例子中,所有这4个声明的常量都是类型确定的。 X
和Y
的类型都是float32
, A
和B
的类型都是int64
。
const X float32 = 3.14
const (
A, B int64 = -3, 5
Y float32 = 2.718
)
我们也可以使用显式类型转换来声明类型确定常量。 下面的例子和上面的例子是完全等价的。
const X = float32(3.14)
const (
A, B = int64(-3), int64(5)
Y = float32(2.718)
)
欲将一个字面常量绑定到一个类型确定有名常量上,此字面常量必须能够表示为此常量的确定类型的值。 否则,编译将报错。
const a uint8 = 256 // error: 256溢出uint8
const b = uint8(255) + uint8(1) // error: 256溢出uint8
const c = int8(-128) / int8(-1) // error: 128溢出int8
const MaxUint_a = uint(^0) // error: -1溢出uint
const MaxUint_b uint = ^0 // error: -1溢出uint
在上面的例子中,符号^
为位反运算符,符号+
为加法运算符,符号/
为除法运算符。
下面这个类型确定常量声明在64位的操作系统上是合法的,但在32位的操作系统上是非法的。 因为一个uint
值在32位操作系统上的尺寸是32位, (1 << 64) - 1
将溢出。(这里,符号<<
为左移位运算符。)
const MaxUint uint = (1 << 64) - 1
那么如何声明一个代表着最大uint
值的常量呢? 我们可以用下面这个常量声明来替换上面这个。下面这个声明在64位和32位的操作系统上都是合法的。
const MaxUint = ^uint(0)
类似地,我们可以使用下面这个常量声明来声明一个有名常量来表示最大的int
值。(这里,符号>>
为右移位运算符。)
const MaxInt = int(^uint(0) >> 1)
使用类似的方法,我们可以声明一个常量来表示当前操作系统的位数,或者检查当前操作系统是32位的还是64位的。
const NativeWordBits = 32 << (^uint(0) >> 63) // 64 or 32
const Is64bitOS = ^uint(0) >> 63 != 0
const Is32bitOS = ^uint(0) >> 32 == 0
这里,符号!=
和==
分别为不等于和等于比较运算符。
常量声明中的自动补全
在一个包含多个常量描述的常量声明中,除了第一个常量描述,其它后续的常量描述都可以只有标识符部分。 Go编译器将通过照抄前面最紧挨的一个完整的常量描述来自动补全不完整的常量描述。 比如,在编译阶段,编译器会将下面的代码
自动补全为
const (
X float32 = 3.14
Y float32 = 3.14
Z float32 = 3.14
A, B = "Go", "language"
C, _ = "Go", "language"
)
在常量声明中使用iota
iota
是Go中预声明(内置)的一个特殊的有名常量。 iota
被预声明为0
,但是它的值在编译阶段并非恒定。 当此预声明的iota
出现在一个常量声明中的时候,它的值在第n个常量描述中的值为n
(从0开始)。 所以iota
只对含有多个常量描述的常量声明有意义。
iota
和常量描述自动补全相结合有的时候能够给Go编程带来很大便利。 比如,下面是一个使用了这两个特性的例子。 请阅读代码注释以了解清楚各个常量被绑定的值。
package main
func main() {
k = 3 // 在此处,iota == 0
m float32 = iota + .5 // m float32 = 1 + .5
n // n float32 = 2 + .5
p = 9 // 在此处,iota == 3
q = iota * 2 // q = 4 * 2
_ // _ = 5 * 2
r // r = 6 * 2
s, t = iota, iota // s, t = 7, 7
u, v // u, v = 8, 8
_, w // _, w = 9, 9
)
const x = iota // x = 0 (iota == 0)
const (
y = iota // y = 0 (iota == 0)
z // z = 1
)
println(m) // +1.500000e+000
println(n) // +2.500000e+000
println(q, r) // 8 12
println(s, t, u, v, w) // 7 7 8 8 9
println(x, y, z) // 0 0 1
}
上面的例子只是展示了一下如何使用iota
。 在实际编程中,我们应该用有意义的方式使用之。比如:
const (
Failed = iota - 1 // == -1
Unknown // == 0
Succeeded // == 1
)
const (
Readable = 1 << iota // == 1
Writable // == 2
Executable // == 4
)
在上面这段代码中,-
是一个减法运算符。
变量声明和赋值操作语句
变量可以被看作是在运行时刻存储在内存中并且可以被更改的有名字的值。
所有的变量值都是类型确定值。当声明一个变量的时候,我们必须在代码中给编译器提供足够的信息来让编译器推断出此变量的确切类型。
在一个函数体内声明的变量称为局部变量。 在任何函数体外声明的变量称为包级或者全局变量。
Go语言有两种变量声明形式。一种称为标准形式,另一种称为短声明形式。 短声明形式只能用来声明局部变量。
标准变量声明形式
每条标准变量声明形式语句起始于一个var
关键字。 每个var
关键字跟随着一个变量名。 每个变量名必须为一个。
下面是几条完整形式的标准变量声明语句。 这些声明确地指定了被声明的变量的类型和初始值。
var lang, website string = "Go", "https://golang.org"
var compiled, dynamic bool = true, false
var announceYear int = 2009
我们可以看到,和常量声明一样,多个同类型的变量可以在一条语句中被声明。
完整形式的标准变量声明使用起来有些罗嗦,因此很少在日常Go编程中使用。 在日常Go编程中,另外两种变种形式用得更广泛一些。 一种变种形式省略了变量类型(但仍指定了变量的初始值),这时编译器将根据初始值的字面量形式来推断出变量的类型。 另一种变种形式省略了初始值(但仍指定了变量类型),这时编译器将使用变量类型的零值做为变量的初始值。
下面是一些第一种变种形式的用例。在这些用例中,如果一个初始值是一个类型确定值,则对应声明的变量的类型将被推断为此初始值的类型; 如果一个初始值是一个类型不确定值,则对应声明的变量的类型将被推断为此初始值的默认类型。 注意在这种变种中,同时声明的多个变量的类型可以不一样。
// 变量lang和dynamic的类型将被推断为内置类型string和bool。
var lang, dynamic = "Go", false
// 变量compiled和announceYear的类型将被推断
// 为内置类型bool和int。
var compiled, announceYear = true, 2009
// 变量website的类型将被推断为内置类型string。
var website = "https://golang.org"
上例中的类型推断可以被视为隐式类型转换。
下例展示了几个省略了初始值的标准变量声明。每个声明的变量的初始值为它们各自的类型的零值。
var lang, website string // 两者都被初始化为空字符串。
var interpreted, dynamic bool // 两者都被初始化为false。
var n int // 被初始化为0。
和常量声明一样,多个变量可以用一对小括号组团在一起被声明。
var (
lang, bornYear, compiled = "Go", 2007, true
announceAt, releaseAt int = 2009, 2012
createdBy, website string
)
上面这个变量声明语句已经被go fmt
命令格式化过了。 这个变量声明语句包含三个变量描述(variable specification)。
一般来说,将多个相关的变量声明在一起将增强代码的可读性。
纯赋值语句
在上面展示的变量声明的例子中,等号=
表示赋值。 一旦一个变量被声明之后,它的值可以被通过纯赋值语句来修改。 多个变量可以同时在一条赋值语句中被修改。
一个赋值语句等号左边的表达式必须是一个可寻址的值、一个映射元素或者一个空标识符。 内存地址(以及指针)和映射将在以后的文章中介绍。
空标识符也可以出现在纯赋值语句的左边,表示不关心对应的目标值。 空标识符不可被用做源值。
一个包含了很多(合法或者不合法的)纯赋值语句的例子:
const N = 123
var x int
var y, z float32
N = 789 // error: N是一个不可变量
y = N // ok: N被隐式转换为类型float32
x = y // error: 类型不匹配
x = N // ok: N被隐式转换为类型int
y = x // error: 类型不匹配
z = y // ok
_ = y // ok
z, y = y, z // ok
_, y = y, z // ok
z, _ = y, z // ok
_, _ = y, z // ok
x, y = 69, 1.23 // ok
x, y = y, x // error: 类型不匹配
x, y = int(y), float32(x) // ok
上例中的最后一行使用了显式类型转换,否则此赋值(见倒数第二行)将不合法。 数字非常量值的类型转换规则将在后边的章节介绍。
Go不支持某些其它语言中的连等语法。下面的赋值语句在Go中是不合法的。
var a, b int
a = b = 123 // 语法错误
短变量声明形式
我们也可以用短变量声明形式来声明一些局部变量。比如下例:
每个短声明语句中必须至少有一个新声明的变量。
从上面的例子中,我们可以看到短变量声明形式和标准变量声明形式有几个显著的区别:
- 短声明形式不包含
var
关键字,并且不能指定变量的类型。 - 短变量声明中的赋值符号必须为
:=
。 - 在一个短声明语句的左侧,已经声明过的变量和新声明的变量可以共存。 但在一个标准声明语句中,所有出现在左侧的变量必须都为新声明的变量。
注意,相对于纯赋值语句,目前短声明语句有一个限制:出现在一个短声明左侧的项必须都为纯标识符。 以后我们将学习到在纯赋值语句的左边可以出现结构体值的字段,指针的解引用和容器类型值的元素索引项等。 但是这些项不能出现在一个变量短声明语句的左边。
关于“赋值”这个术语
以后,当“赋值”这个术语被提到的时候,它可以指一个纯赋值、一个短变量声明或者一个初始值未省略的标准变量声明。 事实上,一个更通用的定义包括后续文章将要介绍的。
当y = x
是一条合法的赋值语句时,我们可以说x
可以被赋给y
。 假设y
的类型为Ty
,有时为了叙述方便,我们也可以说x
可以被赋给类型Ty
。
一般来说,如果x
可以被赋给,则y
应该是可修改的,并且x
和y
的类型相同或者x
可以被隐式转换到y
的类型。 当然,y
也可以是空标识符_
。
每个局部声明的变量至少要被有效使用一次
注意,当使用目前的主流Go编译器编译Go代码时,一个局部变量被声明之后至少要被有效使用一次,否则编译器将报错。 包级变量无此限制。 如果一个变量总是被当作赋值语句中的目标值,那么我们认为这个变量没有被有效使用过。
下面这个例子编译不通过。
package main
var x, y, z = 123, true, "foo" // 包级变量
func main() {
var q, r = 789, false
r, s := true, "bar"
r = y // r没有被有效使用。
x = q // q被有效使用了。
}
当编译上面这个程序的时候,编译器将报错(这个程序代码存在一个名为example-unused.go
的文件中):
./example-unused.go:6:6: r declared and not used
./example-unused.go:7:16: s declared and not used
避免编译器报错的方法很简单,要么删除相关的变量声明,要么像下面这样,将未曾有效使用过的变量(这里是r
和s
)赋给空标识符。
package main
var x, y, z = 123, true, "foo"
func main() {
var q, r = 789, false
r, s := true, "bar"
r = y
x = q
_, _ = r, s // 将r和s做为源值使用一次。
}
若干包级变量在声明时刻的依赖关系将影响它们的初始化顺序
下面这个例子中的声明的变量的初始化顺序为y = 5
、c = y
、b = c+1
、a = b+1
、x = a+1
。
var x, y = a+1, 5 // 8 5
var a, b, c = b+1, c+1, y // 7 6 5
包级变量在初始化的时候不能相互依赖。比如,下面这个变量声明语句编译不通过。
var x, y = y, x
值的可寻址性
在Go中,有些值是可以被寻址的。上面已经提到所有变量都是可以寻址的,所有常量都是不可被寻址。 我们可以从后面的一文了解更多关于内存地址和指针的知识。
在Go中,两个类型不一样的值是不能相互赋值的。 我们必须使用显式类型转换将一个值转换为另一个值的类型之后才能进行赋值。
前面某节已经提到了整数(不论常量还是非常量)都可以被显式转换为字符串类型。 这里再介绍两个不同类型数字值之间的转换规则。
- 一个非常量浮点数和整数可以显式转换到其它任何一个浮点数和整数类型。
- 一个非常量复数可以显式转换到其它任何一个复数类型。
上面已经提到,常量数字值的类型转换不能溢出。此规则不适用于非常量数字值的类型转换。 非常量数字值的类型转换中,溢出是允许的。 另外当将一个浮点数非常量值(比如一个变量)转换为一个整数类型的时候,舍入(或者精度丢失)也是允许的。 具体规则如下:
- 当从一个比特位数多的整数类型的非常量整数值向一个比特位数少的整数类型转换的时候,高位的比特将被舍弃,低位的比特将被保留。我们称这种处理方式为截断(truncated)。
- 当从一个非常量的浮点数向一个整数类型转换的时候,浮点数的小数部分将被舍弃(向零靠拢)。
- 当从一个非常量整数或者浮点数向一个浮点数类型转换的时候,精度丢失是可以发生的。
- 当从一个非常量复数向另一个复数类型转换的时候,精度丢失也是可以发生的。
- 当一个显式转换涉及到非常量浮点数或者复数数字值时,如果源值溢出了目标类型的表示范围,则转换结果取决于具体编译器实现(即行为未定义)。
在下面的例子中,第7行和第15行的隐式转换是不允许的,第5行和第14行的显式转换也是不允许的。
const a = -1.23
// 变量b的类型被推断为内置类型float64。
var b = a
// error: 常量1.23不能被截断舍入到一个整数。
var x = int32(a)
// error: float64类型值不能被隐式转换到int32。
var y int32 = b
// ok: z == -1,变量z的类型被推断为int32。
// z的小数部分将被舍弃。
var z = int32(b)
const k int16 = 255
var n = k // 变量n的类型将被推断为int16。
var f = uint8(k + 1) // error: 常量256溢出了uint8。
var g uint8 = n + 1 // error: int16值不能隐式转换为uint8。
var h = uint8(n + 1) // ok: h == 0,变量h的类型为uint8。
// (n+1)溢出uint8,所以只有低8位
// bits(都为0)被保留。
第3行的隐式转换中,a
被转换为它的默认类型(float64
);因此b
的类型被推断为float64
。
变量和常量的作用域
在Go中,我们可以使用一对大括号来显式形成一个(局部)代码块。一个代码块可以内嵌另一个代码块。 最外层的代码块称为包级代码块。 一个声明在一个内层代码块中的常量或者变量将遮挡另一个外层代码块中声明的同名变量或者常量。 比如,下面的代码中声明了3个名为x
的变量。 内层的x
将遮挡外层的x
, 从而外层的x
在内层的x
声明之后在内层中将不可见。
package main
const y = 70
var x int = 123 // 包级变量
func main() {
// 此x变量遮挡了包级变量x。
var x = true
// 一个内嵌代码块。
{
x, y := x, y-10 // 这里,左边的x和y均为新声明
// 的变量。右边的x为外层声明的
// bool变量。右边的y为包级变量。
// 在此内层代码块中,从此开始,
// 刚声明的x和y将遮挡外层声明x和y。
x, z := !x, y/10 // z是一个新声明的变量。
// x和y是上一句中声明的变量。
println(x, y, z) // false 60 6
}
println(x) // true
println(y) // 70 (包级变量y从未修改)
/*
println(z) // error: z未定义。
// z的作用域仅限于上面的最内层代码块。
*/
}
刚提到的作用域是指一个标识符的可见范围。 一个包级变量或者常量的作用域为其所处于的整个代码包。 一个局部变量或者常量的作用域开始于此变量或者常量的声明的下一行,结束于最内层包含此变量或者常量的声明语句的代码块的结尾。 这解释了为什么上例中的println(z)
将编译不通过。
后面的一文将详述代码块和标识符的作用域。
更多关于常量声明
一个类型不确定常量所表示的值可以溢出其默认类型
比如,下例中的三个类型不确定常量均溢出了它们各自的默认类型,但是此程序编译和运行都没问题。
package main
// 三个类型不确定常量。
const n = 1 << 64 // 默认类型为int
const r = 'a' + 0x7FFFFFFF // 默认类型为rune
const x = 2e+308 // 默认类型为float64
func main() {
_ = n >> 2
_ = r - 0x7FFFFFFF
_ = x / 2
}
但是下面这个程序编译不通过,因为三个声明的常量为类型确定常量。
package main
// 三个类型确定常量。
const n int = 1 << 64 // error: 溢出int
const r rune = 'a' + 0x7FFFFFFF // error: 溢出rune
const x float64 = 2e+308 // error: 溢出float64
func main() {}
每个常量标识符将在编译的时候被其绑定的字面量所替代
常量声明可以看作是增强型的C语言中的#define
宏。 在编译阶段,所有的标识符将被它们各自绑定的字面量所替代。
如果一个运算中的所有运算数都为常量,则此运算的结果也为常量。或者说,此运算将在编译阶段就被估值。 下一篇文章将介绍Go中的。
上面这段程序代码将在编译阶段被重写为下面这样:
package main
var a = 3
func main() {
b := 6