在上一讲的末尾,我们使用循环结构实现了查找10以内的素数。但这并不意味着结束,因为查找往往意味着使用。而要保存和访问找出的这一组数据,就需要使用数组了。

这节课,我们就来介绍一下Go语言中的数据“容器”。本节包括以下内容:

  1. Go语言中的数组、切片的声明和赋值
  2. 集合的声明和赋值

# 数组

我们先来回顾查找10以内素数的代码实现:

go
复制代码func main() {
   for i := 2; i < 10; i++ {
      //假定i为素数
      flag := true
      for j := 2; j < i; j++ {
         if i%j == 0 {
            //当i能被某个整数整除时,不是素数
            flag = false
         }
      }
      //如果依旧为true,则i为素数
      if flag {
         //输出素数i
         fmt.Println(i)
      }
   }
}

运行后,控制台输出:

2 3 5 7

显然,上述代码中当flag为true时输出的单个i值均为素数,总共有4个。为了方便在后续的代码中使用这4个数,我们将其放置在一个数据“组合”中。像这样的数据“组合”,可以使用数组来管理,数组中的每个值又称为数组中的“元素”。

❗️ 注意: 数组中的元素并不限制值的类型,但要求所有元素均为相同的类型。

对于本例而言,便可声明一个总共有4个元素的数组来管理素数结果。Go语言中声明数组的一般格式为:

go
复制代码var array_name [quantity]Type

其中,var关键字用于声明变量;array_name表示数组名;quantity表示数组元素个数;Type表示元素类型。

因此,对于本例,便可声明名为“resultArray”的数组变量,其元素个数为4,类型为int,代码为:

go
复制代码var resultArray [4]int

声明后即可为单个元素赋值了。和其它的编程语言类似,要为数组中的某个元素赋值,格式为:

go
复制代码array_name[index] = Value

其中,array_name表示已声明的数组名;index表示数组中的第N个元素,称之为索引或下标,通常是包含0的正整数;Value则表示具体的数据值。

对于本例而言,若要为数组中的第3个元素赋值为5,对应的代码为:

go
复制代码resultArray[2] = 5
❗️ 注意: 数组的索引从0开始,因此索引的取值范围应该是从0至数组元素个数减1为止。如本例则为0123。超出范围的赋值和取值将引发下标越界错误,导致程序出错。对某一索引位置的元素重复赋值将导致旧值被新值替换。

到此,数组的声明和赋值方法都有了,下面就来将它们结合到原有代码中吧!

go
复制代码func main() {
   var resultArray [4]int
   var arrayIndex int = 0
   for i := 2; i < 10; i++ {
      //假定i为素数
      flag := true
      for j := 2; j < i; j++ {
         if i%j == 0 {
            //当i能被某个整数整除时,不是素数
            flag = false
         }
      }
      //如果依旧为true,则i为素数
      if flag {
         //将素数存放到resultArray数组中
         resultArray[arrayIndex] = i
         arrayIndex++
      }
   }
   fmt.Println(resultArray)
}

如上代码所示,除了声明resultArray外,还声明了arrayIndex作为数组赋值时的索引之用。每次赋值结束后arrayIndex都会自增1,以便为下个元素赋值。

运行这段代码,控制台输出:

[2 3 5 7]

如此,查找到的结果集便保存到了resultArray数组中,后面的代码便通过resultArray随时访问结果集了。

接下来思考一个问题,本例要求查找了10以内的素数。如果换成查找30以内、50以内,甚至更大范围的话,查找到的结果必然会有不同程度的增加。

如果使用数组来存放结果的话,很容易引发下标越界错误。和数组相对,Go语言还提供了一种专门存放不定元素个数的数据结构——切片。

# 切片

对于已经熟悉数组声明和赋值的朋友来说,使用切片并非难事。在Go语言中,切片的声明一般格式为:

go
复制代码var slice_name []Type

其中,var关键字用于声明变量;slice_name表示切片名;Type表示元素类型。

💡 提示: 注意到了吗?声明切片和数组的区别仅仅是去掉了中括号中的元素个数!

对于本例,代码实现为:

go
复制代码var resultSlice []int
❗️ 注意: 和数组类似,切片中的元素也不限制值的类型,但要求所有元素均为相同的类型。

完成切片的声明后,就来到赋值环节。

与数组不同,为切片赋值可以理解为“扩充”。在一开始,切片里面的元素个数为0。“扩充”一个值,就相当于为切片中的第一个元素赋值。赋值后,切片的元素个数就变成了1。若再次“扩充”,则相当于为切片中的第二个元素赋值。赋值后,切片的元素个数就变成了2,以此类推……

在Go语言中,为切片“扩充”需要使用append()函数,使用格式如下:

go
复制代码slice_name = append(slice_name, value)

其中,slice_name表示已声明的切片变量名,value表示具体的数据值。

值得一提的是,append()函数本身并不会改变原有切片,只是将切片“扩容”后的结果作为函数返回值。因此,需要将“扩容”后的结果再次(即函数返回值)赋值给slice_name,才能真正使slice_name发生改变。

对于本例而言,若要“扩充”切片,添加值为2的元素,代码实现为:

go
复制代码resultSlice = append(resultSlice, value)

到此,我们掌握了切片的声明和赋值方法,接下来又到了修改原有代码的时候。修改后的代码如下:

go
复制代码func main() {
   var resultSlice []int
   for i := 2; i < 30; i++ {
      //假定i为素数
      flag := true
      for j := 2; j < i; j++ {
         if i%j == 0 {
            //当i能被某个整数整除时,不是素数
            flag = false
         }
      }
      //如果依旧为true,则i为素数
      if flag {
         //将素数存放到resultArray数组中
         resultSlice = append(resultSlice, i)
      }
   }
   fmt.Println(resultSlice)
}

显然,由于切片在赋值时无需关注下标,因此连原有的arrayIndex变量也省了。

运行这段代码,控制台将输出:

[2 3 5 7 11 13 17 19 23 29]

接下来,如果想查找100以内的素数,该如何修改呢?答案是——只需要修改循环终止的条件即可(即将i < 30改为i < 100),是不是更方便呢?

在实际项目中,切片的使用其实更为广泛。例如:当用户发起搜索,搜索的结果个数往往会根据搜索关键字的不同而发生变化。在不确定总数的前提下,使用数组显然是不合适的。

# 集合

使用数组不合适,那使用什么呢?答案是:集合。我们考虑另一个场景——管理学生信息。

如何保存和查找一所学校所有学生的信息呢?通过姓名显然是不合适的,因为会有重名的情况。通过年级+姓名呢?显然也是不合适的,因为这可能会执行两次筛选,而且也无法从根本上排除重名的情况。所以,我们应使用一个能标识一个学生唯一性的数据作为保存和查找的依据。比如:学号。

一个学号对应一个学生,保存时如此,查找时亦如此。

💡 提示: 与此类似的管理方式还有身份证号、驾驶证号、商品ID、图书ISBN等等。这些能标识唯一性的值可以统称为“唯一ID”。在实际项目中,为了保证单条数据的唯一性,为其构建唯一ID号是非常有必要的。

像上述这种唯一ID对应单条数据,可以使用集合来管理。集合可以看作是一类特殊的切片,只不过集合的元素都是由若干“键-值对”数据构成的。所谓“键”,相当于“唯一ID”;“值”,相当于单条数据,键不允许重复

Go语言中声明集合的一般格式为:

go
复制代码var map_name = make(map[key_type]value_type)
❗️ 注意: 和数组、切片类似,集合中的键和值均不限制数据类型,且键和值可分别使用不同的类型。但要求所有键均为相同的类型,所有值均为相同类型。

其中,var用于声明变量;map_name表示集合的变量名;key_type表示键的类型;value_type表示值的类型。

对于本例而言,学号和学生信息都使用string类型来表示,集合的变量名为studentInfos。对应的代码为:

go
复制代码var studentInfos = make(map[string]string)

集合中元素的赋值和数组类似,只不过中括号中不再是索引,而是键的值。例如,保存学号为“0001”的学生,名为“王小红”,代码实现为:

go
复制代码studentInfos["0001"] = "王小红"

以此类推,继续增加4条信息,完整的代码如下:

go
复制代码func main() {
   var studentInfos = make(map[string]string)
   studentInfos["0001"] = "王小红"
   studentInfos["0002"] = "李小明"
   studentInfos["0003"] = "张三丰"
   studentInfos["0004"] = "孙小贝"
   studentInfos["0005"] = "何明明"
   // 输出语句
   fmt.Println(studentInfos)
}

程序运行结果为:

map[0001:王小红 0002:李小明 0003:张三丰 0004:孙小贝 0005:何明明]

值得一提的是,若对一个已经存在数据的“键”再次赋值,原有的数据将被覆盖。比如,在本例输出语句前添加:

go
复制代码studentInfos["0003"] = "周丹"

再次运行本例,结果则变为:

map[0001:王小红 0002:李小明 0003:周丹 0004:孙小贝 0005:何明明]

在实际项目中,充分利用集合键唯一的性质,还可以确保排除重复的数据。

# 小结

🎉 恭喜,您完成了本次课程的学习!

📌 以下是本次课程的重点内容总结:

  1. Go语言中的数组、切片;
  2. Go语言中集合的声明和赋值。

数组、切片和集合都是Go语言中的数据“容器”。使用它们可以将多个数据成组保存,以备后用。

数组是固定长度的,切片是不定长度的。集合是一类特殊的切片,元素由若干键-值对构成,键不允许重复。

在本讲中,无论是数组、切片抑或是集合,获取元素的方式都是一股脑输出的。有没有办法只获取其中某个元素呢?另外,如何获取它们所拥有的元素个数呢?这些问题,我们将在下一讲中一一解答。

➡️ 在下次课程中,我们会介绍如下内容:

  • Go语言中的数据“容器”,包括:
    • 数组、切片和集合的元素遍历、访问和修改