• Go语言中的复合数据类型
  • 发布于 2个月前
  • 208 热度
    0 评论
所谓复合数据类型,就是由多种基本数据类型组合而成的功能更强大的类型,就像原子构成分子一样,复合数据类型在我们的程序会承担更繁杂的工作。Go 语言里常用的数据类型有四种:分别是数组、slice(切片)、map(集合)以及结构体。其中,数组和结构体的长度在定义的时候就固定好了,而 slice 和 map 可以实现动态增长。

数组
数组是具有固定长度,且拥有零或多个相同数据类型元素的序列。数组元素通过它的索引来访问,索引的值从 0 到 数组长度减 1,即 [0,len(arr) - 1] (Go 内置函数 len 可以返回数组中的元素个数)。
 var arr [3]int // 声明一个int类型的数组,长度为3
 fmt.Println(arr[len(arr)-1]) // 输出最后一个元素,即arr[2],此处为0
 for i, v := range arr {
     fmt.Printf("arr[%d]=%d ", i, v) // range循环,输出索引和元素
 }
 for _, v := range arr { // 堆代码 duidaima.com
     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] = 0
 fmt.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 := 2
 a = 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 Person
 p.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 Wheel
 w.Circle.Center.X = 6
 w.Circle.Center.Y = 6
 w.Circle.Redis = 5
 w.Spokes = 20
但是,Go 提供了一种简便方法,允许我们定义只需要指定类型,而不带名称的结构体成员,即匿名成员。嵌套关系的访问,可以省略中间的匿名成员。于是,我们的代码可以这样修改一下:
 type Circle struct {
     Point // 省略了变量的名字
     Redis int
 }
 
 type Wheel struct {
     Circle // 省略了变量的名字
     Spokes int
 }
此时,访问 Wheel 的变量可以简化为:
 var w Wheel
 w.X = 6 //等价于 w.Circle.Center.X = 6
 w.Y = 6
 w.Redis = 5
 w.Spokes = 20

总结
我们在建造房子时,几乎很难精细地考虑到要用到多少砖头、瓦块或者金属;但是对窗户、大门等复杂构造模块的数量是可以有大概预估的,基本数据类型和复杂数据类型也是一样。

一门编程语言学习之初,不应纯粹追求对语法的熟练度;而是在需要的时候,可以知道有这种应用场景,能快速地查询到使用的方法。毕竟复制粘贴的工作做多了,也就熟练了(/手动狗头)。

又是一年 10 月,砖也没那么烫手了。我希望,你们向往的冬季,是温暖的火炉与暖气,是北方人眼前的银装素裹,是南方人梦里的风和日丽。
用户评论