一些合法的转换例子:
go// 结果为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"
下面是一些非法的转换:
goint(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)
Go支持类型推断(type deduction or type inference)。
类型推断是指在某些场合下,程序员可以在代码中使用一些类型不确定值, 编译器会自动推断出这些类型不确定值在特定情景下应被视为某些特定类型的值
在Go代码中,如果某处需要一个特定类型的值并且一个类型不确定值可以表示为此特定类型的值, 则此类型不确定值可以使用在此处。Go编译器将此类型不确定值视为此特定类型的类型确定值。 这种情形常常出现在运算符运算、函数调用和赋值语句中。
有些场景对某些类型不确定值并没有特定的类型要求。在这种情况下,Go编译器将这些类型不确定值视为它们各自的默认类型的类型确定值。
gopackage 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白皮书把上面每行含有一个等号=的语句称为一个常量描述。
每个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)。 包级常量也常常被称为全局常量。
我们可以在声明一些常量的时候指定这些常量的确切类型。 这样声明的常量称为类型确定具名常量。 在下面这个例子中,所有这4个声明的常量都是类型确定的。 X和Y的类型都是float32, A和B的类型都是int64。
goconst X float32 = 3.14
const (
A, B int64 = -3, 5
Y float32 = 2.718
)
我们也可以使用显式类型转换来声明类型确定常量。 下面的例子和上面的例子是完全等价的。
goconst X = float32(3.14)
const (
A, B = int64(-3), int64(5)
Y = float32(2.718)
)
欲将一个字面常量绑定到一个类型确定具名常量上,此字面常量必须能够表示为此常量的确定类型的值。 否则,编译将报错。
goconst 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将溢出uint。(这里,符号<<为左移位运算符。)
goconst MaxUint uint = (1 << 64) - 1
那么如何声明一个代表着最大uint值的常量呢? 我们可以用下面这个常量声明来替换上面这个。下面这个声明在64位和32位的操作系统上都是合法的。
goconst MaxUint = ^uint(0)
类似地,我们可以使用下面这个常量声明来声明一个具名常量来表示最大的int值。(这里,符号>>为右移位运算符。)
goconst MaxInt = int(^uint(0) >> 1)
使用类似的方法,我们可以声明一个常量来表示当前操作系统的位数,或者检查当前操作系统是32位的还是64位的。
goconst NativeWordBits = 32 << (^uint(0) >> 63) // 64 or 32
const Is64bitOS = ^uint(0) >> 63 != 0
const Is32bitOS = ^uint(0) >> 32 == 0
在一个包含多个常量描述的常量声明中,除了第一个常量描述,其它后续的常量描述都可以只包含标识符列表部分。 Go编译器将通过照抄前面最紧挨的一个完整的常量描述来自动补全不完整的常量描述。 比如,在编译阶段,编译器会将下面的代码
goconst (
X float32 = 3.14
Y // 这里必须只有一个标识符
Z // 这里必须只有一个标识符
A, B = "Go", "language"
C, _
// 上一行中的空标识符是必需的(如果
// 上一行是一个不完整的常量描述的话)。
)
自动补全为
goconst (
X float32 = 3.14
Y float32 = 3.14
Z float32 = 3.14
A, B = "Go", "language"
C, _ = "Go", "language"
)
iota是Go中预声明(内置)的一个特殊的具名常量。 iota被预声明为0,但是它的值在编译阶段并非恒定。 当此预声明的iota出现在一个常量声明中的时候,它的值在第n个常量描述中的值为n(从0开始)。 所以iota只对含有多个常量描述的常量声明有意义。
iota和常量描述自动补全相结合有的时候能够给Go编程带来很大便利。 比如,下面是一个使用了这两个特性的例子。 请阅读代码注释以了解清楚各个常量被绑定的值。
gopackage 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。 在实际编程中,我们应该用有意义的方式使用之。比如:
goconst (
Failed = iota - 1 // == -1
Unknown // == 0
Succeeded // == 1
)
const (
Readable = 1 << iota // == 1
Writable // == 2
Executable // == 4
)
每条标准变量声明形式语句起始于一个var关键字。 每个var关键字跟随着一个变量名。 每个变量名必须为一个标识符。
下面是几条完整形式的标准变量声明语句。 这些声明确地指定了被声明的变量的类型和初始值。
govar lang, website string = "Go", "https://golang.org"
var compiled, dynamic bool = true, false
var announceYear int = 2009
完整形式的标准变量声明使用起来有些罗嗦,因此很少在日常Go编程中使用。 在日常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"
上例中的类型推断可以被视为隐式类型转换。
下例展示了几个省略了初始值的标准变量声明。每个声明的变量的初始值为它们各自的类型的零值
govar lang, website string // 两者都被初始化为空字符串。
var interpreted, dynamic bool // 两者都被初始化为false。
var n int // 被初始化为0。
和常量声明一样,多个变量可以用一对小括号组团在一起被声明。
govar (
lang, bornYear, compiled = "Go", 2007, true
announceAt, releaseAt int = 2009, 2012
createdBy, website string
)
上面这个变量声明语句已经被go fmt命令格式化过了。 这个变量声明语句包含三个变量描述(variable specification)。
一般来说,将多个相关的变量声明在一起将增强代码的可读性。
在上面展示的变量声明的例子中,等号=表示赋值。 一旦一个变量被声明之后,它的值可以被通过纯赋值语句来修改。 多个变量可以同时在一条赋值语句中被修改。
一个赋值语句等号左边的表达式必须是一个可寻址的值、一个映射元素或者一个空标识符。 内存地址(以及指针)和映射将在以后的文章中介绍。
常量是不可改变的(不可寻址的),所以常量不能做为目标值出现在纯赋值语句的左边,而只能出现在右边用做源值。 变量既可以出现在纯赋值语句的左边用做目标值,也可以出现在右边用做源值。
空标识符也可以出现在纯赋值语句的左边,表示不关心对应的目标值。 空标识符不可被用做源值。
一个包含了很多(合法或者不合法的)纯赋值语句的例子:
goconst 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中是不合法的。
govar a, b int
a = b = 123 // 语法错误
我们也可以用短变量声明形式来声明一些局部变量。比如下例:
gopackage 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()
}
每个短声明语句中必须至少有一个新声明的变量。
从上面的例子中,我们可以看到短变量声明形式和标准变量声明形式有几个显著的区别:
以后,当“赋值”这个术语被提到的时候,它可以指一个纯赋值、一个短变量声明或者一个初始值未省略的标准变量声明。 事实上,一个更通用的定义包括后续文章将要介绍的函数传参。
当y = x是一条合法的赋值语句时,我们可以说x可以被赋给y。 假设y的类型为Ty,有时为了叙述方便,我们也可以说x可以被赋给类型Ty。
一般来说,如果x可以被赋给y,则y应该是可修改的,并且x和y的类型相同或者x可以被隐式转换到y的类型。 当然,y也可以是空标识符_。
注意,当使用目前的主流Go编译器编译Go代码时,一个局部变量被声明之后至少要被有效使用一次,否则编译器将报错。 包级变量无此限制。 如果一个变量总是被当作赋值语句中的目标值,那么我们认为这个变量没有被有效使用过。
gopackage 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的文件中):
go./example-unused.go:6:6: r declared and not used
./example-unused.go:7:16: s declared and not used
避免编译器报错的方法很简单,要么删除相关的变量声明,要么像下面这样,将未曾有效使用过的变量(这里是r和s)赋给空标识符。
gopackage 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。
govar x, y = a+1, 5 // 8 5
var a, b, c = b+1, c+1, y // 7 6 5
包级变量在初始化的时候不能相互依赖。比如,下面这个变量声明语句编译不通过。
govar x, y = y, x
在Go中,两个类型不一样的基本类型值是不能相互赋值的。 我们必须使用显式类型转换将一个值转换为另一个值的类型之后才能进行赋值。
具体规则如下:
在下面的例子中,第7行和第15行的隐式转换是不允许的,第5行和第14行的显式转换也是不允许的。
goconst 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声明之后在内层中将不可见。
gopackage 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)将编译不通过。
一个类型不确定常量所表示的值可以溢出其默认类型 比如,下例中的三个类型不确定常量均溢出了它们各自的默认类型,但是此程序编译和运行都没问题。
gopackage main
// 三个类型不确定常量。
const n = 1 << 64 // 默认类型为int
const r = 'a' + 0x7FFFFFFF // 默认类型为rune
const x = 2e+308 // 默认类型为float64
func main() {
_ = n >> 2
_ = r - 0x7FFFFFFF
_ = x / 2
}
但是下面这个程序编译不通过,因为三个声明的常量为类型确定常量。
gopackage 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中的常用运算符。
gopackage main
const X = 3
const Y = X + X
var a = X
func main() {
b := Y
println(a, b, X, Y)
}
gopackage main
var a = 3
func main() {
b := 6
println(a, b, 3, 6)
}
本文只介绍算术运算符、位运算符、比较运算符、布尔运算符和字符串衔接运算符。 这些运算符要么是二元的(需要两个操作数),要么是一元的(需要一个操作数)。 所有这些运算符运算都只返回一个结果。操作数常常也称为操作值。
本文中的解释不追求描述的完全准确性。 比如,当我们说一个二元运算符运算需要其涉及的两个操作数类型必须一样的时,这指:
在继续下面的章节之前,我们需要知道什么叫常量表达式和关于常量表达式估值的一个常识。 表达式的概念将在表达式和语句一文中得到解释。 目前我们只需知道本文中所提及的大多数运算都属于表达式。 当一个表达式中涉及到的所有操作数都是常量时,此表达式称为一个常量表达式。 一个常量表达式的估值是在编译阶段进行的。一个常量表达式的估值结果依然是一个常量。 如果一个表达式中涉及到的操作数中至少有一个不为常量,则此表达式称为非常量表达式。
GO支持5个基本二元算术运算符:
字面形式 | 名称 | 对两个运算数的要求 |
---|---|---|
+ | 加法 | 两个运算符数的类型必须相同并且为基本数值类型 |
- | 减法 | 同上 |
* | 乘法 | 同上 |
/ | 除法 | 同上 |
% | 余数 | 两个运算数的类型必须相同并且为基本整数数值类型 |
go支持六种位运算符(也属于算术运算)
字面形式 | 名称 | 对两个运算数的要求及机制解释 |
---|---|---|
& | 位与 | 两个操作数的类型必须相同并且为基本整数数值类型。机制解释(下标2表明一个字面量为二进制):11002 & 10102 得到 10002 11002 |
位或 | ||
^ | (位)异或 | 同上 |
&^ | 清位 | 同上 |
字面形式 | 名称 | 对两个运算数的要求及机制解释 |
---|---|---|
<< | 左移位 | 左操作数必须为一个整数,右操作数也必须为一个整数(如果它是一个常数,则它必须非负),但它们的类型可以不同。 (注意:在Go 1.13之前,右操作数必须为一个无符号整数类型的类型确定值或者一个可以表示成uint值的类型不确定常数值。)一个负右操作数(非常数)将在运行时刻造成一个恐慌。机制解释:11002 << 3 得到 11000002(低位补零)11002 >> 3 得到 12(低位被舍弃) 注意,在右移运算中,左边空出来的位(即高位)全部用左操作数的最高位(即正负号位)填充。 比如如果左操作数-128的类型为int8(二进制补码表示为100000002), 则100000002 >> 2的二进制补码结果为111000002(即-32)。 |
>> | 右移位 | 同上 |
Go也支持三个一元算术运算符
字面形式 | 名称 | 对两个运算数的要求及机制解释 |
---|---|---|
+ | 取正数 | +n等价于0 + n. |
- | 取负数 | -n等价于0 - n. |
^ | 位反(或位补) | ^n等价于m ^ n,其中m和n同类型并且它的二进制表示中所有比特位均为1。 比如如果n的类型为int8,则m的值为-1;如果n的类型为uint8,则m的值为255。 |
注意:
一些运算符的使用示例:
gofunc main() {
var (
a, b float32 = 12.0, 3.14
c, d int16 = 15, -6
e uint8 = 7
)
// 这些行编译没问题。
_ = 12 + 'A' // 两个类型不确定操作数(都为数值类型)
_ = 12 - a // 12将被当做a的类型(float32)使用。
_ = a * b // 两个同类型的类型确定操作数。
_ = c % d
_, _ = c + int16(e), uint8(c) + e
_, _, _, _ = a / b, c / d, -100 / -9, 1.23 / 1.2
_, _, _, _ = c | d, c & d, c ^ d, c &^ d
_, _, _, _ = d << e, 123 >> e, e >> 3, 0xF << 0
_, _, _, _ = -b, +c, ^e, ^-1
// 这些行编译将失败。
_ = a % b // error: a和b都不是整数
_ = a | b // error: a和b都不是整数
_ = c + e // error: c和e的类型不匹配
_ = b >> 5 // error: b不是一个整数
_ = c >> -5 // error: -5不是一个无符号整数
_ = e << uint(c) // 编译没问题
_ = e << c // 从Go 1.13开始,此行才能编译过
_ = e << -c // 从Go 1.13开始,此行才能编译过。
// 将在运行时刻造成恐慌。
_ = e << -1 // error: 右操作数不能为负(常数)
}
上一篇文章提到了
go// 结果为非常量
var a, b uint8 = 255, 1
var c = a + b // c==0。a+b是一个非常量表达式,
// 结果中溢出的高位比特将被截断舍弃。
var d = a << b // d == 254。同样,结果中溢出的
// 高位比特将被截断舍弃。
// 结果为类型不确定常量,允许溢出其默认类型。
const X = 0x1FFFFFFFF * 0x1FFFFFFFF // 没问题,尽管X溢出
const R = 'a' + 0x7FFFFFFF // 没问题,尽管R溢出
// 运算结果或者转换结果为类型确定常量
var e = X // error: X溢出int。
var h = R // error: R溢出rune。
const Y = 128 - int8(1) // error: 128溢出int8。
const Z = uint8(255) + 1 // error: 256溢出uint8。
本文作者:Eric
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!