引言
前面的博文,我们介绍了 Go 语言的各种数据类型,包括基本数据类型、数组类型、切片类型、字典类型、函数类型、接口类型、结构体类型和指针类型;从本篇开始我们一起来了解 Go 语言数据的使用。
主要内容
1. 赋值语句
如果值 x 可以被赋给类型为 T 的变量,那么它们至少需要满足以下条件中的一个赋值规则:
如果值 x 的类型是 T , 那么 x 可以被赋给 T 类型的变量。
如果值 x 的类型是 V,那么 V 和 T 应该具有相同的潜在类型,并且它们之中至少有一个是未命名的类型。未命名的类型是指未被署名的数据类型。例如,字面量:
1
2
3
4struct {
a int
b string
}{0,"string"}所代表的值的类型是
1
2
3
4struct {
a int
b string
}而这个类型就是一个未命名的类型。它的潜在类型与结构体类型
1
2
3
4type Anonym struct {
a int
b string
}相同。因此,上面的字面量可以被赋给类型为 Anonym 的变量。
类型 T 是一个接口类型,且值 x 的类型实现了 T。因此,x 就可以被赋给类型为 T 的变量。
如果值 x 是一个双向通道类型的值,而 T 也是一个通道类型。那么 x 的类型 V 和 T 应该具有相同的元素类型,并且它们之中至少有一个是未命名的类型。(在之后讲述通道类型的时候详细说明)
如果值 x 预定义标识符 nil,那么它可以被赋给切片类型、字典类型、函数类型、接口类型、指针类型和通道类型的变量。只要变量不是值类型,它就可以被赋予空值 nil。
如果值 x 是一个由某个数据类型的值代表的无类型的常量(可以理解为字面量),那么他就可以被赋给该数据类型的变量。例如,字符串字面量“ABC”可以被赋给 string 类型的变量,以及整数字面量 123 可以被赋给 int 类型的变量。
所有值都可以被赋给 **空标识符”_”**。空标识符有时也被称为占位标识符。它只起到占位的作用,不会与任何值建立绑定关系。
赋值语句一般由左右分立的两个表达式列表和处于中间的一个赋值操作符组成。例如:
1 | var ints = []int{1, 2, 3} |
表达式列表中的多个表达式之间需要有逗号作为分隔符。在大多数情况下,左右两边的表达式的数量必须是相同的。当左边表达式的数量大于 1 时,就形成了多个赋值操作同时进行的情况,这种情况常常被称为平行赋值。
在赋值操作符左边的那个表达式列表中的每个表达式的结果值都必须是可寻址的。如果不需要对复制操作符右边的某个表达式的结果值进行绑定,可以在赋值操作符左边的相应位置上应用 **空标识符”_”**。例如:
1 | ints[1], _ = (ints[1] + 1), (ints[2] + 1) |
对于普通赋值语句(以=为赋值操作符的赋值语句)来说,在赋值操作符两边的表达式的数量可以不相等。当在赋值操作符的右边只有一个表达式且该表达式是一个多值表达式(与单值表达式相对应)的时候,在赋值操作符的左边的表达式可以有多个。这时右边唯一的表达式有4种情况:
此表达式是一个调用会返回多个结果的函数或者方法的表达式。这时,在赋值操作符的左边的表达式的数量应该等于该函数或方法的结果的数量。
此表达式是一个应用于字典值之上的索引表达式。这时,在赋值操作符的左边可以有一个或两个表达式。例如:
1
v, ok := map["k1"]
此表达式是一个类型断言表达式。这时,在赋值操作符的左边可以有一个或两个表达式。例如:
1
v, ok := x.(string)
此表达式是一个由接收操作符和通道类型值组成的表达式。这时,在赋值操作符的左边可以有一个或两个表达式。例如:
1
v, ok := <-ch
赋值语句的执行分为两个阶段:
第一个阶段,在赋值操作符左边的索引表达式和取址表达式的操作数以及在赋值操作符右边的表达式,都会按照通常的顺序被求值。
第二个阶段,赋值会以从左到右的顺序进行。
在 Go 语言中可以使用平行赋值来交换两个变量的值:
1 | a, b = b, a |
由于平行赋值永远是从左向右进行的,所以即使靠右的赋值引发了运行时恐慌,它左边的赋值也依然生效。
在赋值语句中,每个右边的表达式的结果值必须是可以被赋给与其相对应的左边的表达式的类型的,即使这些值由无类型的常量代表。
2. 常量与变量
常量一旦被声明它的值就不能被改变,而对于变量却没有这样的限制。
2.1 常量
在 Go 语言中,常量总会在编译期间被创建,即使它们作为局部变量被定义在了函数内部。常量只能由字面量常量或常量表达式来赋值。常量表达式是能够且会在编译期间被求值的。而其他的表达式只能在程序运行期间被求值,所以它们并不能被赋给常量。
Go 语言的常量:布尔常量、rune常量(也成为字符常量)、整数字面量、浮点数字面量、复数字面量或字符串字面量表示。由相应的字面量表示的基本数据类型的值都可以被称为常量值。由字面量表示的常量值也简称为字面常量。例如,布尔字面量true是一个字面常量。
常量可以是有类型的也可以是没有类型的。有字面量表示的常量,如true、false、“A”和 iota 以及由仅以无类型的常量作为其操作数的常量表达式的结果值都属于无类型的常量。
从语言规范上来说,数组型常量可以有任意的精度,但编译器却只会使用一个有限精度的内部表示方法来实现它们。对于每一个实现,都必须满足一下条件:
整数字面量至少要用256个比特位来表示。
浮点数字面量的小数部分至少要用256个比特位来表示,而其指数部分至少要用32个比特位来表示。对于复数常量的实部和虚部中的相应部分也是如此。
若不能精确地表示一个整数常量,则要给出一个错误。
若由于溢出而不能表示一个浮点数常量或复数常量,则要给出一个错误。
若由于精度限制而不能表示一个浮点数常量或复数常量,则这个值会被四舍五入为一个可表示的最相近的常量。
2.1.1 常量表达式
常量表达式就是仅以常量作为操作数的表达式。无类型的布尔常量、数值常量和字符串常量都可以作为常量表达式的操作数。如果一个二元操作的操作数是两个不同种类的无类型的数组型常量,那么对于非布尔操作(不包含比较操作符的操作)来说其操作结果的种类遵循着这样的优先级顺序(从高到低):复数、浮点数、rune 和整数。例如:
1 | 2 + 3.0 // 结果是一个无类型的浮点数常量5.0 |
操作数为无类型常量的移位操作的结果总会是一个无类型的整数常量。例如:
1 | 1 << 3.0 // 结果是一个无类型的整数常量8 |
比较操作结果总会是一个无类型的布尔常量。例如:
1 | "A" > "C" // 结果是一个无类型的布尔常量false |
常量表达式总会被正确地求值。中间值和作为表达式结果的常量值自身都会有足够的精度。这个精度可以比 Go 语句中预定义的那些类型所支持的精度更高。例如:
1 | 1 << 100 // 结果是一个无类型的整数常量1267650600228229401496703205376 |
这个常量值实际上已经超出了 Go 语言中任何一个整数类型(即使是 uint64 类型)所能表示的范围了。
对于有类型的常量来说,它的值必须永远能够被精确地表示为其类型的值。
2.1.2 常量的声明
常量声明会将字面量或常量表达式与标识符绑定在一起。与变量不同的是,对常量的赋值必须与其声明同时进行。并且,只能对常量赋值一次。
一条常量声明语句总会以关键字 const 开始。例如:
1 | const untypedConstant = 10.0 // 无类型的常量的声明 |
在常量声明语句中还可以包含平行赋值。如下:
1 | const tc1, tc2, tc3 int64 = 1024, -10, 88 |
在包含平行赋值的常量声明语句中,如果类型被给定,那么所有的常量的类型都应该与这个被给定的类型一致。相应的,赋值操作符右边的字面常量或常量表达式的结果值也都可以被赋给这个类型。
如果在包含平行赋值的常量声明语句中为给定类型,那么赋值操作符右边的多个字面量或常量表达式的结果值的种类都会是彼此独立的,即它们的种类都可以是任意的。例如:
1 | // 标识符 utc1, utc2, utc3分别表示了一个浮点数常量,一个布尔常量和一个字符串常量。 |
将上面的常量声明语句进行拆分,如下:
1 | const ( |
在这个圆括号中的每一行都可以被称为一个常量声明。在圆括号中的常量声明内,同样可以进行平行赋值:
1 | const ( |
在带圆括号的常量声明语句中,有时候并不需要显式地对所有的常量进行赋值。被省略了赋值的常量实际上还是有值的,只不过这个值是被隐含地赋予了。它们的值及其类型都会与上面的,最近的且被显式赋值的那个常量相同。因此对第一个被声明的常量的赋值是永远不能够被省略的。例如:
1 | const ( |
尽管可以被省略,但是还是有两个与此有关的约束:
如果有一个未被显示赋值的常量,那么与它同一行的常量(如果有的话)的赋值也都必须被省略。
在未包含显示赋值的那一行常量中的常量标识符的数量必须与在它上面的、最近的且包含显式赋值的那一行常量声明中的常量标识符的数量相等。例如:
1
2
3
4const (
utc1, utc2, utc3 = 6.3, false, "C"
utc4, utc5
) // 不合法,不符合约束2,会造成一个编译错误。可以这样解决:
1
2
3
4
5const (
utc1 = 6.3
utc2, utc3 = false, "C"
utc4, utc5
)
在 Go 语言中不但可以为隐含地多个常量赋予同一个值,而且还可以更加方便地对多个常量分别赋予一系列连续的值。例如:
1 | const { |
在常量声明语句中,iota 代表了连续的、无类型的整数常量。它第一次出现在一个以 const 开始的常量声明语句中的时候总会表示整数常量 0。
在同一条常量声明语句中,iota 在第二个包含它的常量声明中会表示为整数常量 1,在第三个包含它的常量声明中会表示为 2,以此类推。随着在同一条常量声明语句中包含 iota 的常量声明的数量的增加,iota 所表示的整数值也会递增。
1 | const x = iota // 常量x的值是整数常量0 |
利用 iota 进行更加灵活的常量隐式赋值。例如:
1 | const { |
在同一条常量声明语句中,iota 代表的整数常量的值是否递增取决于是否又有一个常量声明包含了它,而不是它是否又在常量声明中出现了一次。例如:
1 | const { |
Go 语言中可以利用 空标识符”_” 来跳过 iota 表示的递增序列中的某个或某些值。例如:
1 | const { |
总结:对常量进行声明的时候必须同时对它进行赋值。对一个常量的赋值只能进行一次,且只有字面常量和常量表达式可以作为它的值。可以利用 隐式赋值、平行赋值和 itoa 对常量进行非常灵活和复杂的赋值操作。
2.2 变量
变量 与 常量 的最主要的区别是它在被声明之后可以被赋值任意次。对于变量来说,它的值是可以在程序运行期间才被计算出来的。
2.2.1 变量声明
一个变量声明可以将一个标志符与一个变量值绑定在一起。前提条件是这个变量值与该变量的类型之间必须要满足赋值规则。
变量声明语句总是会以关键字 var 开始:
1 | var v int64 = 0 |
我们也可以省略变量的类型:
1 | var v = 0 |
如果变量的类型未被显式地指定,那么它将会由变量值推导得出。如果在省略类型的同时,赋值操作符右边的表达式的求值结果是一个字面常量,那么该变量的类型将会根据这个字面常量的种类被推导出来。
字面常量与变量类型的对应关系
字面常量的种类 | 变量的类型 |
---|---|
布尔常量 | bool |
字符常量 | rune |
浮点数常量 | float64 |
复数常量 | complex128 |
字符串常量 | string |
以上的对应情况下,Go 语言的运行时程序会根据字面常量的种类将其转换为对应的数据类型,然后在赋给相应的变量。
在Go语言中同样可以对多个变量进行平行赋值:
1 | var v1, v2 = 0, -1 |
把多个变量的声明拆分成多行:
1 | var ( |
注意: 隐式赋值 在变量声明中是不可用的。
在Go语言中,可以不对一个新声明的变量的值进行初始化。如果初始化的显示赋值被省略,那么变量的值将会是与该变量的类型相对应的零值。这时,变量的类型不可以被省略。如果是平行赋值的话,要么省略其中所有变量的初始赋值,要么就必须对所有变量进行初始赋值。例如:
1 | var v3, v4, v5 float64 |
或者必须这样:
1 | var v3, v4, v5 float64 = 3, 4, 5 |
2.2.2 局部变量
与常量相同,变量声明可以作为源码文件中的顶级元素,也可以成为函数体内容的一部分。前者可以称为全局变量,后者可以被称为某个函数的局部变量。局部变量有时也被称为本地变量。
在函数体内部,局部变量会遮蔽与它同名的全局变量。例如:
1 | packge main |
在函数内部声明变量的时候可以采用一种简单方式,前面已经涉及过,如下:
1 | v6 := true // 短变量声明,根据它的值推出变量的类型 |
短变量声明也不需要以 var 开始,这种特殊标记 := 只会被用于对变量的声明和初始化的语句中,所以并不会产生歧义。短变量声明与普通变量声明一样,也支持平行赋值。例如:
1 | v7, v8 := "Go", 1.2 |
重声明仅会出现在短变量声明中,可以理解为对当前的已存在变量的又一次赋值。例如:
1 | v8, v9 := 2.0, false |
短变量声明的约束条件:
- 短变量声明仅能够在函数体内部声明变量的时候使用。
- 在短变量声明中的:=的左边的标识符至少要有一个代表在当前上下文环境中的新变量。
注意 :空标识符”_” 代表的并不是新的变量,即使它在当前上下文环境中并没有出现过。
短变量声明可以出现在 if、for 和 switch 等语句的初始化器中,并被用来声明仅存在于这些语句块中的本地临时变量。这些知识点在之后的博文讲述的Go语言流程控制方法中介绍。
如果我们在当前上下文环境中声明了某个局部变量但没有使用它,那么就会造成一个编译错误。Go 语言认为这种情况是对计算机资源的浪费,甚至预示着更加严重的问题的出现。
注意:对变量的赋值并不算是对它的使用。
3. 可比性与有序性
3.1 类型的恒等
别名类型和它的源类型是两个完全不同的类型。一个命名类型和一个匿名类型总是不恒等的。如果两个匿名类型的类型字面量是相同的,就可以说它们是恒等的。
各个数据类型的恒等判断方法的规则:
对于两个数组类型,如果它们的长度一致且元素的数据类型是恒等的,那么它们就是恒等的。
1
2
3
4[4]string
[4]string//这两个数组类型就是恒等的
[4]string
[3]string//这两个数组类型就是不恒等的。数组类型的长度是类型声明的一部分,也是类型的一部分
对于两个切片类型,如果它们的元素的数据类型恒等,那么它们就是恒等的。
1
2[]string
[]string//这两个切片类型是恒等的。切片类型的长度并不会存在于类型声明中,并且两个同一切片类型的值的长度也不一定会相同。
对于两个结构体类型来说,如果它们之中的字段声明的数量是相同的,并且在对应位置上的字段具有相同的字段名称(如果有的话)和恒等的数据类型,那么这两个结构体数据类型就是恒等的。
1
2
3
4
5
6
7
8
9var a1 struct {
f1 sort.Interface
f2 int64
}
var a2 struct {
f1 sort.Interface
f2 int64
}从字面看,变量 a1 和变量 a2 的类型显然是恒等的。如果其中一个结构体类型声明中有匿名函数,那么在另一个结构体类型的声明中的对应位置上的字段声明也必须不包含名称,否则,它们就是不相等的。如果结构体类型的某个字段声明是有标签的,那么这个标签也应该作为恒等判断的一个依据。
对两个指针类型,如果它们的基本类型(也就是它们指向的那个类型)恒等,那么它们就是恒等的。
对于两个函数类型,如果它们包含了相同数量的参数声明和结果声明,并且在对应位置上的参数或结果的类型都是恒等的,那么它们就是恒等的。
1
2func(name string, dept string, isManager bool) (id int, done bool)
func(appName string, targetOs string, authRequired bool) (id int, doen bool)函数类型的恒等判断并不会以参数和结果的名称为依据,而只关心它的数量、顺序和类型。如果其中一个函数类型是可变参函数,那么另一个函数类型也应该是这样,否则它们就是不恒等的。
对于两个接口类型,如果它们拥有相同的方法集合,那么它们就是恒等的。两个接口类型所包含的方法声明的数量必须相同,并且对于一个接口类型中包含的每一个方法声明都能够在另一个接口类型中找到与它完全相等的方法声明。两个接口类型中的方法声明的顺序是无关紧要的。如下两个是恒等的:
1
2
3
4
5
6
7
8
9type Ia interface {
Name() string
Age() int
}
type Ib interface {
Age() int
Name() string
}对于两个字典类型,如果它们具有恒等的键类型和元素类型,那么它们就是恒等的。
对于两个通道类型,如果它们具有恒等的元素类型,并且方向相同,那么它们就是恒等的。(之后的博文中会详细的介绍)
注意: 如果两个数据类型在不同的代码包中,即使它们满足了上述相关规则也是不相等的。
3.2 数据的可比性与有序性
上面的类型的恒等阐述了 Go 语言的数据类型之间的可比性,下面关注的是数据类型的值之间的可比性和有序性。可比性 是可以判断相等与否,有序性 是可以比较大小的含义。
各个数据类型的值的相关特性:
布尔类型值具有可比性。布尔值只有 true 和 false 两种可能。两个布尔值可以判断是否相等,却无法比较两个布尔值的大小。
整数类型值具有可比性,也具有有序性。
浮点数类型值具有可比性,也具有有序性。这被定义在IEEE-754标准中(一个针对二进制浮点数的算术标准)。
复数类型值具有可比性。判断两个复数类型值是否相等的结果是通过分别对它们的实部和虚部上的值进行比较而得出的。
字符串类型值具有可比性,也具有有序性。两个字符串类型值判断相等或比较大小的方法就是对它们中的每个对应位置上的字节进行判断或比较。这就相当于对多对整数类型值依次进行判断或比较,直到可以得出结果为止。
指针类型具有可比性。如果两个指针类型值指向了同一个变量,或者它们都为空值 nil,那么就可以判定它们是相等的。例如:
1
2
3
4numArray := [3]int{1, 23, 456}
p1 := &numArray
p2 := &numArray
fmt.Printf("%v\n", p1 == p2) // 打印true通道类型值具有可比性。如果两个通道类型值的元素类型和缓冲区大小都一致,那么就可以被判定为相等。如果两个通道类型的变量的值都是 nil ,那么它们也是相等的。
接口类型值具有可比性。如果两个接口类型值拥有相等的动态类型和相同的动态值,那么就可以判定它们是相等的。如果有一个接口类型的变量,那么在这个变量中就只能存储实现了该接口类型的类型的值。把存储在该变量中的那个值的类型叫作该变量的动态类型,而把这个值叫作该变量的动态值。如果两个接口类型的变量的值都是空值,那么它们也是相等的。例如:
1
2
3
4
5
6
7
8
9
10
11type Ic interface {
Code() string
}
type Sc struct {
code string
}
func (self Sc) Code() string {
return self.code
}结构体类型 Sc 是接口类型 Ic 的一个实现类型。如果有两个 Ic 类型的变量:
1
2
3var ic1 Ic = Sc{code: "A"}
var ic2 Ic = Sc{code: "A"}
fmt.Printf("%v\n", ic1 == ic2) // 打印true非接口类型 X 的值 x 可以与接口类型 T 的值 t 判断相等,当且仅当接口类型 T 具有可比性且类型 X 是接口类型 T 的实现类型。新增一个变量声明:
1
2
3var sc1 Sc = Sc{code: "A"}
fmt.Printf("%v\n", ic1 == sc1) // 打印true
fmt.Printf("%v\n", ic2 == sc1) // 打印true如果一个结构体类型中的所有字段都具有可比性,那么这个结构体类型的值就具有可比性。如果两个结构体值中的对应的字段值是相等的,那么这两个结构体类型值就是相等的。例如:
1
2
3
4
5
6type Sd struct {
ints []int
}
sd1 := Sd{ints: []int{0, 1}}
sd2 := Sd{ints: []int{0, 1}}
fmt.Printf("%v\n", sd1 == sd2)//被编译的时候就会造成一个编译错误结构体类型 Sc 中包含了一个切片类型的字段,而切片类型的值是不具有可比性的。
数组类型值具有可比性,当前仅当其元素类型的值具有可比性。如果两个数组类型值在对应位置上的值都是相等的,那么这两个数组类型值就是相等的。例如:
1
2
3slices1 := [3][]int{[]int{0, 1}}
slices2 := [3][]int{[]int{0, 1}}
fmt.Printf("%v\n", slices1 == slices2)//被编译的时候就会造成一个编译错误变量 slice1 和 slice2 都代表了元素类型为 [ ]int 的数组类型的值。这两个值的类型的元素类型都是不具有可比性的,从而这两个数组类型的值也不具有可比性。
在判断两个具有相同接口类型的值是否相等的时候,如果它们的动态类型不具有可比性就会引发一个运行恐慌。比如,两个接口类型的变量的动态类型是 切片类型 或 字典类型 的 别名类型。同样适用于如下情况,以接口类型为元素类型的数组类型的值,以及以接口类型为其中某个字段的类型的结构体类型的值。
1 | type Se []int |
注意: 切片类型、字典类型 和 函数类型 的值是不具有可比性的。但这些值可以与空值 nil 进行判等的。
结语
下篇继续讲解 Go 语言数据的使用,主要包括 类型转换 和 内建函数 的介绍。
最后附上知名的 Go 语言开源框架:
NSQ: 一个实时的分布式消息平台。它拥有很高的可伸缩性,并能够每天处理数以十亿计的消息。它的官方网址是:http://nsq.io