顺序编程-5

3.7 数组
      数组是Go语言编程中最常用的数据结构之一。顾名思义,数组就是指一系列同一类型数据

的集合。数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。

以下为一些常规的数组声明方法:
             [32]byte                                    // 长度为32的数组,每个元素为一个字节
             [2*N] struct { x, y int32 }          // 复杂类型数组
             [1000]*float64                        // 指针数组
             [3][5]int                                 // 二维数组
             [2][2][2]float64                     // 等同于[2]([2]([2]float64))

从以上类型也可以看出,数组可以是多维的,比如 [3][5]int 就表达了一个3行5列的二维整型数组,总共可以

存放15个整型元素。在Go语言中,数组长度在定义后就不可更改,在声明时长度可以为一个常量或者一个常量表达式(常量表达式是指在编译期即可计算结果的表达式)。数组的长度是该数组类型的一个内置常量,可以用Go语言的内置函数 len() 来获取。下面是一个获取数组 arr 元素个数的写法:

                                                    arrLength := len(arr)

(1). 元素访问

可以使用数组下标来访问数组中的元素。与C语言相同,数组下标从0开始, len(array)-1则表示最后一个元素的下标。下面的示例遍历整型数组并逐个打印元素内容:

                            for i := 0; i < len(array); i++ {
                            fmt.Println("Element", i, "of array is", array[i])
                              }

Go语言还提供了一个关键字 range ,用于便捷地遍历容器中的元素。当然,数组也是 range的支持范围。上面的遍历过程可以简化为如下的写法:

                                        for i, v := range array {

                                        fmt.Println("Array element[", i, "]=", v)
                                          }
在上面的例子里可以看到, range 具有两个返回值,第一个返回值是元素的数组下标,第二
个返回值是元素的值。
(2). 值类型
需要特别注意的是,在Go语言中数组是一个值类型(value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。
    下面用例子来说明这一特点:
                            package main
                            import "fmt"
                            func modify(array [10]int) {
                            array[0] = 10                     // 试图修改数组的第一个元素
                            fmt.Println("In modify(), array values:", array)
                                 }
                          func main() {
                          array := [5]int{1,2,3,4,5}          // 定义并初始化一个数组
                          modify(array)                        // 传递给一个函数,并试图在函数体内修改这个数组内容
                          fmt.Println("In main(), array values:", array)
                                   }
该程序的执行结果为:
                      In modify(), array values: [10 2 3 4 5]
                      In main(), array values: [1 2 3 4 5]
从执行结果可以看出,函数 modify() 内操作的那个数组跟 main() 中传入的数组是两个不同的实例。
3.8 数组切片
在前一节里学习到了数组的特点:数组的长度在定义之后无法再次修改;数组是值类型,
每次传递都将产生一份副本。显然这种数据结构无法完全满足开发者的真实需求。
Go语言提供了数组切片(slice)这个非常酷的功能来弥补数组的不足。
初看起来,数组切片就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是
个指针。数组切片的数据结构可以抽象为以下3个变量:
                              一个指向原生数组的指针;
                              数组切片中的元素个数;
                              数组切片已分配的存储空间。
从底层实现的角度来看,数组切片实际上仍然使用数组来管理元素,因此它们之间的关系让
C++程序员们很容易联想起STL中 std::vector 和数组的关系。基于数组,数组切片添加了一系
列管理功能,可以随时动态扩充存放空间,并且可以被随意传递而不会导致所管理的元素被重复

复制。

  (1). 创建数组切片
        创建数组切片的方法主要有两种——基于数组和直接创建,下面我们来简要介绍一下这两种
方法。
        a.  基于数组
   数组切片可以基于一个已存在的数组创建。数组切片可以只使用数组的一部分元素或者整个
  数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片。代码清单2-1演示了如何基
  于一个数组的前5个元素创建一个数组切片。

代码清单3-1 slice.go
             package main
             import "fmt"
            func main() {
           // 先定义一个数组
            var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
           // 基于数组创建一个数组切片
          var mySlice []int = myArray[:5]
          fmt.Println("Elements of myArray: ")
          for _, v := range myArray {
           fmt.Print(v, " ")
             }
          fmt.Println("\nElements of mySlice: ")
           for _, v := range mySlice {
           fmt.Print(v, " ")
               }
             fmt.Println()
               }
运行结果为:
Elements of myArray:
1 2 3 4 5 6 7 8 9 10
Elements of mySlice:
1 2 3 4 5
应该已经注意到,Go语言支持用 myArray[first:last] 这样的方式来基于数组生成一
个数组切片,而且这个用法还很灵活,比如下面几种都是合法的。
          基于 myArray 的所有元素创建数组切片:
                        mySlice = myArray[:]
         基于 myArray 的前5个元素创建数组切片:
                       mySlice = myArray[:5]

          基于从第5个元素开始的所有元素创建数组切片:
                          mySlice = myArray[5:]
           b. 直接创建
并非一定要事先准备一个数组才能创建数组切片。Go语言提供的内置函数 make() 可以用于
灵活地创建数组切片。下面的例子示范了直接创建数组切片的各种方法。
创建一个初始元素个数为5的数组切片,元素初始值为0:
                   mySlice1 := make([]int, 5)
创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:
                  mySlice2 := make([]int, 5, 10)
直接创建并初始化包含5个元素的数组切片:
                  mySlice3 := []int{1, 2, 3, 4, 5}
当然,事实上还会有一个匿名数组被创建出来,只是不需要我们来操心而已。
(2). 元素遍历
操作数组元素的所有方法都适用于数组切片,比如数组切片也可以按下标读写元素,用 len()
函数获取元素个数,并支持使用 range 关键字来快速遍历所有元素。
             传统的元素遍历方法如下:
                    for i := 0; i <len(mySlice); i++ {
                    fmt.Println("mySlice[", i, "] =", mySlice[i])
                      }
使用 range 关键字可以让遍历代码显得更整洁。 range 表达式有两个返回值,第一个是索引,
第二个是元素的值:
                      for i, v := range mySlice {
                          fmt.Println("mySlice[", i, "] =", v)
                           }
对比上面的两个方法,我们可以很容易地看出使用 range 的代码更简单易懂。
(3). 动态增减元素
      可动态增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能
力(capacity)的概念,即元素个数和分配的空间可以是两个不同的值。合理地设置存储能力的
值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序性能。
假如你明确知道当前创建的数组切片最多可能需要存储的元素个数为50,那么如果你设置的
存储能力小于50,比如20,那么在元素超过20时,底层将会发生至少一次这样的动作——重新分
配一块“够大”的内存,并且需要把内容从原来的内存块复制到新分配的内存块,这会产生比较
明显的开销。给“够大”这两个字加上引号的原因是系统并不知道多大才是够大,所以只是一个
简单的猜测。比如,将原有的内存空间扩大两倍,但两倍并不一定够,所以之前提到的内存重新
分配和内容复制的过程很有可能发生多次,从而明显降低系统的整体性能。但如果你知道最大是
50并且一开始就设置存储能力为50,那么之后就不会发生这样非常耗费CPU的动作,从而达到空

间换时间的效果。
数组切片支持Go语言内置的 cap() 函数和 len() 函数,代码清单3-2简单示范了这两个内置
函数的用法。可以看出, cap() 函数返回的是数组切片分配的空间大小,而 len() 函数返回的是
数组切片中当前所存储的元素个数。
代码清单3-2 slice2.go
                 package main
                 import "fmt"
                 func main() {
                 mySlice := make([]int, 5, 10)
                 fmt.Println("len(mySlice):", len(mySlice))
                 fmt.Println("cap(mySlice):", cap(mySlice))
                }
该程序的输出结果为:
                   len(mySlice): 5
                  cap(mySlice): 10
如果需要往上例中 mySlice 已包含的5个元素后面继续新增元素,可以使用 append() 函数。
下面的代码可以从尾端给 mySlice 加上3个元素,从而生成一个新的数组切片:
                         mySlice = append(mySlice, 1, 2, 3)
函数 append() 的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素,
甚至直接将一个数组切片追加到另一个数组切片的末尾:
                               mySlice2 := []int{8, 9, 10}
                              // 给mySlice后面添加另一个数组切片
                               mySlice = append(mySlice, mySlice2...)
需要注意的是,我们在第二个参数 mySlice2 后面加了三个点,即一个省略号,如果没有这个省
略号的话,会有编译错误,因为按 append() 的语义,从第二个参数起的所有参数都是待附加的
元素。因为 mySlice 中的元素类型为 int ,所以直接传递 mySlice2 是行不通的。加上省略号相
当于把 mySlice2 包含的所有元素打散后传入。
上述调用等同于:
                        mySlice = append(mySlice, 8, 9, 10)
数组切片会自动处理存储空间不足的问题。如果追加的内容长度超过当前已分配的存储空间
(即 cap() 调用返回的信息),数组切片会自动分配一块足够大的内存。
(4). 基于数组切片创建数组切片
     类似于数组切片可以基于一个数组创建,数组切片也可以基于另一个数组切片创建。下面的
例子基于一个已有数组切片创建新数组切片:

                        oldSlice := []int{1, 2, 3, 4, 5}
                        newSlice := oldSlice[:3]        // 基于oldSlice的前3个元素构建新数组切片
有意思的是,选择的 oldSlicef 元素范围甚至可以超过所包含的元素个数,比如 newSlice
可以基于 oldSlice 的前6个元素创建,虽然 oldSlice 只包含5个元素。只要这个选择的范围不超
过 oldSlice 存储能力(即 cap() 返回的值),那么这个创建程序就是合法的。 newSlice 中超出
oldSlice 元素的部分都会填上0。
(5). 内容复制
         数组切片支持Go语言的另一个内置函数 copy() ,用于将内容从一个数组切片复制到另一个
数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行
复制。下面的示例展示了 copy() 函数的行为:
                        slice1 := []int{1, 2, 3, 4, 5}
                        slice2 := []int{5, 4, 3}
                        copy(slice2, slice1)     // 只会复制slice1的前3个元素到slice2中
                        copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

3.9  map
         在C++/Java中, map 一般都以库的方式提供,比如在C++中是STL的 std::map<> ,在C#中是
Dictionary<> ,在Java中是 Hashmap<> ,在这些语言中,如果要使用 map ,事先要引用相应的
库。而在Go中,使用 map 不需要引入任何库,并且用起来也更加方便。
map 是一堆键值对的未排序集合。比如以身份证号作为唯一键来标识一个人的信息,则这个
map 可以定义为代码清单 3-3所示的方式。
代码清单3-3 map1.go
              package main
              import "fmt"
               // PersonInfo是一个包含个人详细信息的类型
              type PersonInfo struct {
              ID string
              Name string
             Address string
                  }
          func main() {
          var personDB map[string] PersonInfo
          personDB = make(map[string] PersonInfo)
               // 往这个map里插入几条数据
          personDB["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."}
          personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."}
             // 从这个map查找键为"1234"的信息
           person, ok := personDB["1234"]

                // ok是一个返回的bool型,返回true表示找到了对应的数据
             if ok {
                fmt.Println("Found person", person.Name, "with ID 1234.")
                      } else {
                fmt.Println("Did not find person with ID 1234.")
                            }
                      }
上面这个简单的例子基本上已经覆盖了 map 的主要用法,下面对其中的关键点进行细述。
(1). 变量声明
map 的声明基本上没有多余的元素,比如:
             var myMap map[string] PersonInfo
其中, myMap 是声明的 map 变量名, string 是键的类型, PersonInfo 则是其中所存放的值类型。
(2). 创建
       我们可以使用Go语言内置的函数 make() 来创建一个新 map 。下面的这个例子创建了一个键
类型为 string 、值类型为 PersonInfo 的 map :
                        myMap = make(map[string] PersonInfo)
也可以选择是否在创建时指定该 map 的初始存储能力,下面的例子创建了一个初始存储能力
为100的 map :
                         myMap = make(map[string] PersonInfo, 100)
关于存储能力,之后应该会学习到。
创建并初始化 map 的代码如下:
                          myMap = map[string] PersonInfo{
                         "1234": PersonInfo{"1", "Jack", "Room 101,..."},
                               }
(3). 元素赋值
赋值过程非常简单明了,就是将键和值用下面的方式对应起来即可:
                    myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."}
(4). 元素删除
Go语言提供了一个内置函数 delete() ,用于删除容器内的元素。下面我们简单介绍一下如
何用 delete() 函数删除 map 内的元素:
                               delete(myMap, "1234")
上面的代码将从 myMap 中删除键为“1234”的键值对。如果“1234”这个键不存在,那么这个调
用将什么都不发生,也不会有什么副作用。但是如果传入的 map 变量的值是 nil ,该调用将导致
程序抛出异常(panic)。
(5). 元素查找
在Go语言中, map 的查找功能设计得比较精巧。而在其他语言中,我们要判断能否获取到一

个值不是件容易的事情。判断能否从 map 中获取一个值的常规做法是:
                            a. 声明并初始化一个变量为空;
                            b. 试图从 map 中获取相应键的值到该变量中;
                            c. 判断该变量是否依旧为空,如果为空则表示 map 中没有包含该变量。
这种用法比较啰唆,而且判断变量是否为空这条语句并不能真正表意(是否成功取到对应的
值),从而影响代码的可读性和可维护性。有些库甚至会设计为因为一个键不存在而抛出异常,
让开发者用起来胆战心惊,不得不一层层嵌套 try-catch 语句,这更是不人性化的设计。在Go
语言中,要从 map 中查找一个特定的键,可以通过下面的代码来实现:
                                   value, ok := myMap["1234"]
                                   if ok { // 找到了
                                     // 处理找到的value
                                      }
判断是否成功找到特定的键,不需要检查取到的值是否为 nil (是指一种类型,无值,任何变量在没有被赋值之前的值都为nil,对于真假判断,只有nil与false、0表示假,其余均为真,与null不一样,null是一个通用指针),只需查看第二个返回值 ok ,这让表意清晰很多。配合 := 操作符,让你的代码没有多余成分,看起来非常清晰易懂。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值