这篇文章将介绍常量和变量相关的知识。 类型不确定值、类型推断和值的显式类型转换等概念也将被介绍。
上一章中提到的基本类型的字面量表示
(除了false和true)都属于无名常量(unnamed constant),或者叫字面常量(literal constant)。
false和true是预声明的两个具名常量。本文将介绍如何声明自定义的具名常量。
在Go中,有些值的类型是不确定的。换句话说,有些值的类型有很多可能性。
这些值称为类型不确定值。对于大多数类型不确定值来说,它们各自都有一个默认类型,
除了预声明的nil。nil是没有默认类型的。
我们在后续的文章中将了解到很多关于nil的知识。
与类型不确定值相对应的概念称为类型确定值。
上一章提到的字面常量(无名常量)都属于类型不确定值。
事实上,Go中大多数的类型不确定值都属于字面常量和本文即将介绍的具名常量。
少数类型不确定值包括刚提到的nil和以后会逐步接触到的某些操作的布尔返回值。
string类型。
bool类型。
int类型。
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不是一个常量,则转换结果也不是一个常量。
// 结果为complex128类型的1.0+0.0i。虚部被舍入了。
complex128(1 + -1e-1000i)
// 结果为float32类型的0.5。这里也舍入了。
float32(0.49999999)
// 只要目标类型不是整数类型,舍入都是允许的。
float32(17000000000000000)
float32(123)
uint(1.0)
int8(-123)
int16(6+0i)
complex128(789)
string(65) // "A"
string('A') // "A"
string('\u68ee') // "森"
string(-1) // "\uFFFD"
string(0xFFFD) // "\uFFFD"
string(0x2FFFFFFFF) // "\uFFFD"
下面是一些非法的转换:
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编译器将这些类型不确定值视为它们各自的默认类型的类型确定值。
上述两条类型推断规则可以被视为隐式转换规则。
本文下面的章节将展示一些类型推断的例子。 后续其它文章将会展示更多类型推断的例子和规则。
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的顺序可以掉换一下。
上面例子中声明的所有常量都是类型不确定的。 它们各自的默认类型和它们各自代表的字面量的默认类型是一样的。
X和Y的类型都是float32,
A和B的类型都是int64。
const X float32 = 3.14
const (
A, B int64 = -3, 5
Y float32 = 2.718
)
如果一个常量描述中包含多个类型确定常量,则这些常量的类型必然是一样的,
比如上例中的A和B。
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
在上面的例子中,符号^为位反运算符,符号+为加法运算符,符号/为除法运算符。
uint值在32位操作系统上的尺寸是32位,
(1 << 64) - 1将溢出uint。(这里,符号<<为左移位运算符。)
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
这里,符号!=和==分别为不等于和等于比较运算符。
const (
X float32 = 3.14
Y // 这里必须只有一个标识符
Z // 这里必须只有一个标识符
A, B = "Go", "language"
C, _
// 上一行中的空标识符是必需的(如果
// 上一行是一个不完整的常量描述的话)。
)
自动补全为
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() {
const (
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 // 语法错误
package main
func main() {
// 变量lang和year都为新声明的变量。
lang, year := "Go language", 2007
// 这里,只有变量createdBy是新声明的变量。
// 变量year已经在上面声明过了,所以这里仅仅
// 改变了它的值,或者说它被重新声明了。
year, createdBy := 2009, "Google Research"
// 这是一个纯赋值语句。
lang, year = "Go", 2012
print(lang, "由", createdBy, "发明")
print("并发布于", year, "年。")
println()
}
每个短声明语句中必须至少有一个新声明的变量。
从上面的例子中,我们可以看到短变量声明形式和标准变量声明形式有几个显著的区别:var关键字,并且不能指定变量的类型。
:=。
注意,相对于纯赋值语句,目前短声明语句有一个限制:出现在一个短声明左侧的项必须都为纯标识符。 以后我们将学习到在纯赋值语句的左边可以出现结构体值的字段,指针的解引用和容器类型值的元素索引项等。 但是这些项不能出现在一个变量短声明语句的左边。
以后,当“赋值”这个术语被提到的时候,它可以指一个纯赋值、一个短变量声明或者一个初始值未省略的标准变量声明。 事实上,一个更通用的定义包括后续文章将要介绍的函数传参。
当y = x是一条合法的赋值语句时,我们可以说x可以被赋给y。
假设y的类型为Ty,有时为了叙述方便,我们也可以说x可以被赋给类型Ty。
一般来说,如果x可以被赋给y,则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中,两个类型不一样的基本类型值是不能相互赋值的。 我们必须使用显式类型转换将一个值转换为另一个值的类型之后才能进行赋值。
前面某节已经提到了整数(不论常量还是非常量)都可以被显式转换为字符串类型。 这里再介绍两个不同类型数字值之间的转换规则。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。
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
const X = 3
const Y = X + X
var a = X
func main() {
b := Y
println(a, b, X, Y)
}
上面这段程序代码将在编译阶段被重写为下面这样:
package main
var a = 3
func main() {
b := 6
println(a, b, 3, 6)
}
Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。
本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。
《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .来浏览和确认各种改动。
本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。
你或许对本书作者老貘开发的一些App感兴趣。
sync标准库包sync/atomic标准库包