所谓复合数据类型,就是由多种基本数据类型组合而成的功能更强大的类型,就像原子构成分子一样,复合数据类型在我们的程序会承担更繁杂的工作。
Go 语言里常用的数据类型有四种:分别是数组、slice(切片)、map(集合)以及结构体。其中,数组和结构体的长度在定义的时候就固定好了,而 slice 和 map 可以实现动态增长。
数组
数组是具有固定长度,且拥有零或多个相同数据类型元素的序列。数组元素通过它的索引来访问,索引的值从 0 到 数组长度减 1,即 [0,len(arr) - 1] (Go 内置函数 len 可以返回数组中的元素个数)。
var arr [3]int // 声明一个int类型的数组,长度为3fmt.Println(arr[len(arr)-1]) // 输出最后一个元素,即arr[2],此处为0for i, v := range arr { fmt.Printf("arr[%d]=%d ", i, v) // range循环,输出索引和元素}for _, v := range arr { fmt.Printf("%d ", v) // 输出数组元素:0 0 0}
如上述所示,一个新数组的初始值为元素类型的零值。对于 int 类型来说,就是 0。所以,我们在定义时,初始化数组:
var arr [3]int = [3]int{1, 2, 3} // 声明一个数组,赋值为{1, 2, 3}var arr2 [3]int = [...]int{1, 2, 3} // 也可用省略号代替赋值数组的长度fmt.Printf("%T", arr2) // 输出数组的类型,此处为 "[3]int"
从这个例子中我们可以看到,数组的类型包含了数组的长度。也就是说,[3]int 和 [4]int 不是一种数据类型:
arr := [3]{1, 2, 3}arr2 := [4]{1, 2, 3, 4}arr = [4]int{1, 2, 3, 4} // 编译错误,不同数据类型之间不可赋值fmt.Println(arr == arr2) // 编译错误,不同数据类型之间不可比较
初始化数组时,我们可以根据索引去确定每个元素的值,索引可以任意顺序出现,也可以省略:
arr := [...]string{1:"b", 0:"a", 3:"c"}fmt.Println(len(arr), arr)// 4 [a b c],索引2被省略了,故arr[2]为空字符串
数组初始化之后,若要对其内部元素值进行修改,可以单独赋值,比如:
arr := [...]int{1, 2, 3, 4}arr[3] = 0fmt.Println(arr)// [1 2 3 0]
但若是想全部更改呢?我们或许可以想到将数组放到函数中处理,但是,当数组作为函数入参时,每个传入的参数都会创建一个副本,然后对副本进行处理,此时原有的数组会维持不变。所以,我们要将传入函数时,需要用到指针:
func zero(p *[4]int) { for i := range p { p[i] = 0}}func main(){ arr := [...]int{1, 2, 3, 4} zero(&arr) fmt.Println(arr) // [0 0 0 0]}
观察上面的例子,我们可以看到 zero 函数接收的入参是一个长度为 4 的 int 数组指针。而前面已经说到,不同长度的数组,数据类型是不一样的。所以,当数组的长度发生变化时,这个函数就不能使用了。
这就是数组天然的限制——不可动态扩容。
因此,除非特殊场景,我们在开发工作中很少会使用数组,而选择长度可动态增长的 slice 切片。
slice
slice 切片是一个可动态增长的序列,和数组一样,它内部的元素都是相同类型的。slice 是一种建立在底层数组之上的轻量级数据结构,可以访问底层数组的部分或者全部元素。
slice 有三个属性:指针、长度和容量,Go 的内置函数 len 和 cap 用来返回 slice 的长度和容量:
一个底层数组可以对应多个 slice,这些 slice 可以引用数组的任意位置,彼此元素间还可以重叠。slice 中的操作符 s[i:j](其中 0<= i <= j <= cap(s)),会返回一个新的引用,这个引用指向索引从 i 到 j 的所有元素(左闭右开:即包含 i 元素,不包含 j 元素)。注意:如果省略了 i,则默认 i = 0;如果省略了 j,则默认 j = len(s):
a := []int{0, 1, 2, 3, 4}b := a[1:3]fmt.Println(b)// [1 2]b = a[:3]// [0 1 2]b = a[2:]// [2 3 4]
因为 slice 本质上是对底层数组的引用,所以当 slice 传递给一个函数时,可以在函数内部直接修改底层数组的元素:
func update(s []int) { s[0] == 100}func main() { arr := []int{0, 1} update(arr) fmt.Println(arr)// [100 1]}
Go 语言中的 append 函数可以将元素追加到 slice 后面,这个函数对于 slice 长度的动态变化很重要。
a := []int{1, 2}a = append(a, 3)// 直接将int元素追加到slice中fmt.Println(a)// [1 2 3]b := []int{4, 5}a = append(a, b...)// 将切片b追加到a中fmt.Println(a)// [1 2 3 4 5]
除了常用的追加功能,append 可以很方便地实现删除当前元素的功能:
a := []int{1, 2, 3, 4}i := 2a = append(a[:i], a[i+1:]...)// 删除第i个元素fmt.Println(a)// [1 2 4]
map
散列表在开发过程中的用途非常广泛,它是一个拥有键值对(key-value)元素的无序集合。这个集合里面,键是唯一的,所以键对应的值可以通过键来获取、更新或者移除。
Go 语言里,map 是散列表的引用,map 的数据类型为 map[K]V。其中 K 和 V 是键和值对应的数据类型,K 的数据类型必须是可以通过操作符 == 进行相等比较的数据类型,比如:基本数据类型 int、string 或 bool;而 V 不仅可以是基本数据类型,也可以是复杂数据类型数组、切片等。
创建一个 map:
ages := make(map[string]int)
对 map 进行赋值:
// 初始化时赋值ages := map[string]int { "zhangs": 18, "lis": 20, // 此处逗号不可省略}// 初始化后赋值ages["wangw"] = 19
在 map 中查找键为 "zangmz" 的元素:
age ,ok := ages["zangmz"]if !ok { fmt.Println("张麻子的年龄没有存入 map 中!")}else { fmt.Println("张麻子的年龄为:", age)}
对 map 进行查询时,第一个元素 age 是元素返回值,二个元素是一个 bool 值,用来判断该元素是否存在,Go 语言中一般将变量名取为 ok。
上面代码中 1 到 4 行我们可以将两条语句合并成一条:
if age, ok := ages["wangmz"]; !ok { ... } else { ... }
当需要从 map 中移除一个元素时,用内置函数 delete 实现:
delete(ages, "zhangs")
结构体
结构体是将零或多个任意类型的命名变量组合在一起的数据类型,Go 语言里的结构体可以理解为其他语言中的对象,但是它的用法相对较简单,且没有复杂的继承概念。首先,我们来定义一个 Person 结构体:
type Person struct { Name string Age int}
type 和 struct 是定义结构体的两个关键字。Person 为结构体变量名,姓名 Name 和年龄 Age 是结构成员,它们可以用点号(.)的方式访问:
var p Personp.Name = "zhangs"p.Age = 18
或者
p := Person{ "zhangs", 18,}
从上我们不难看出,第二种赋值方式,变量的声明顺序需要和结构成员的位置保持一致。
如果结构体的名字是大写字母开头,那么这个结构体就是可导出的。换句话说,结构体名首字母的大小写,决定了该结构体的访问权限。其实成员变量也一样,不过我们在开发中,一般都将结构体和成员的访问保持一致。比如 Person 例子中首字母 P 是大写的,其他包(关于包的概念后续会补充讲解)就可以访问这个结构体和它的成员变量。
匿名成员
前面我们说到了,结构体可以包含任意类型的成员变量。所以,接下来的例子,你将看到我们在开发中常常遇到的结构体组合:
type Point struct { X, Y int}type Circle struct { Center Point Redis int}type Wheel struct { Circle Circle Spokes int}
上面的程序中有三个结构体,分别代表了一个带有 X 和 Y 的点坐标,一个带有圆心和半径的圆盘,以及一个带有圆盘和车轴的轮子。不难发现,它们之间有嵌套的关系。这时,我们想初始化 Wheel 的变量时就变得麻烦了:
var w Wheelw.Circle.Center.X = 6w.Circle.Center.Y = 6w.Circle.Redis = 5w.Spokes = 20
但是,Go 提供了一种简便方法,允许我们定义只需要指定类型,而不带名称的结构体成员,即匿名成员。嵌套关系的访问,可以省略中间的匿名成员。于是,我们的代码可以这样修改一下:
type Circle struct { Point // 省略了变量的名字 Redis int}type Wheel struct { Circle // 省略了变量的名字 Spokes int}
此时,访问 Wheel 的变量可以简化为:
var w Wheelw.X = 6 //等价于 w.Circle.Center.X = 6w.Y = 6w.Redis = 5w.Spokes = 20
总结
我们在建造房子时,几乎很难精细地考虑到要用到多少砖头、瓦块或者金属;但是对窗户、大门等复杂构造模块的数量是可以有大概预估的,基本数据类型和复杂数据类型也是一样。
一门编程语言学习之初,不应纯粹追求对语法的熟练度;而是在需要的时候,可以知道有这种应用场景,能快速地查询到使用的方法。毕竟复制粘贴的工作做多了,也就熟练了(/手动狗头)。
又是一年 12 月,砖也没那么烫手了。我希望,你们向往的冬季,是温暖的火炉与暖气,是北方人眼前的银装素裹,是南方人梦里的风和日丽。