Go语言入门——数组、切片和映射(下)

上篇主要介绍了Go语言里面常见的复合数据类型的声明和初始化。数组

这篇主要针对数组、切片和映射这些复合数据类型从其余几个方面介绍比较下。bash

一、遍历

不论是数组、切片仍是映射结构,都是一种集合类型,要从这些集合取出元素就要查找或者遍历。app

对于从其余语言转到Go语言,在遍历这边仍是有稍稍不一样的。函数

数组遍历

形式1ui

package main

import "fmt"

func main() {
	arr := [5]int{1, 2, 3, 4, 5}

	for i := 0; i < len(arr); i++ {
		fmt.Println(arr[i])
	}
}
复制代码

这种最“老土”的遍历形式应该是全部的语言都通用的吧。spa

形式2指针

package main

import "fmt"

func main() {
	arr := [5]int{1, 2, 3, 4, 5}

	for index, value := range arr  {
		fmt.Println(index, value)
	}
}
复制代码

range关键字表示遍历,后面在切片和映射的遍历咱们也能够看到。code

这个遍历就有点Java里面的加强for的味道了。cdn

可是还有有点不同,我前两天刚写Go代码的时候还在这里掉坑里了。blog

for关键字后面有两个变量,一个是index即数组角标表示第几个元素,一个是value即每一个元素的值。

坑就坑在,若是只有一个变量也是能够遍历的,好比这样

func main() {
	arr := [5]int{1, 2, 3, 4, 5}

	for v := range arr  {
		fmt.Println(v)
	}
}
复制代码

这样和Java的加强for循环遍历几乎就同样了,因此我想固然的觉得这里的v就是arr对应的每一个元素值。

但其实不是,这里v表示的是数组角标。因此若是按照这样的写法本觉得取到的是数组的值,实际上是数组的角标值。

另外,Go语言中有一个特性,对于有些用不上的变量,可使用"_"代替,好比上面的代码能够写成

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	
	for _, value := range arr  {
		fmt.Println(value)
	}
}
复制代码

切片遍历

切片的遍历和数组没有什么区别。

package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4, 5}

	for i := 0; i < len(s); i++ {
		fmt.Println(s[i])
	}

	for index, v := range s {
		fmt.Println(index, v)
	}
}

复制代码

两种遍历方式也都是适用的。

注意这里len函数表示获取切片的长度,除此之外,切片还有一个数组没有的函数即cap,cap表示切片的容量,后面在扩容部分会在提到。

映射遍历

相较于Java里面对于Map遍历与其余集合遍历有些差异来讲,Go里面对于Map的遍历与其余集合的遍历倒显得比较一致。

package main

import "fmt"

func main()  {
	m := make(map[string]string)
	m["Jackie"] = "Zheng"
	m["Location"] = "Shanghai"

	for key, value := range m {
		fmt.Println(key, value)
	}
}
复制代码

除此之外,咱们能够只针对key进行遍历,以下

func main()  {
	m := make(map[string]string)
	m["Jackie"] = "Zheng"
	m["Location"] = "Shanghai"

	for key := range m {
		fmt.Println(key, m[key])
	}
}
复制代码

二、切片扩容

数组和struct结构体都是静态数据,数组是定长的,而切片和映射都是动态数据类型。

为何说是动态数据类型?

上面有顺带提过,切片除了有长度len的概念,还有容量的概念。上篇说到切片声明初始化的一种方式

s := make([]int, 3, 5) // 3所在位置表示切片长度,5所在位置表示容量即最大可能存储的元素个数
复制代码

咱们能够动态向切片添加或者删除元素。

若是新添加元素后已经超出切片原来的容量,那么就会扩容了。借用Go圣经里面的例子

var x, y []int
	for i := 0; i < 10; i++ {
		y = append(x, i)
		fmt.Printf("%d cap=%d\t %v\n", i, cap(y), y)
		x = y
	}
复制代码

使用append添加新元素每次都会校验当前切片的长度若是已经达到最大容量,则会考虑先扩容,从执行结果能够看出每次扩容是原来的两倍,实际的扩容过程是会先建立一个两倍长的底层数组,而后将原切片数据拷贝到这个底层数组,再添加要插入的元素。

因此,这里append函数以后要赋值给对应的切片,由于扩容后和扩容前的内存地址变了,若是不作赋值,可能会出现使用原来的变量没法访问到新切片的状况。

三、传值仍是传引用

首先来看一个数组的例子

package main

import "fmt"

func main() {
	var arr = [5]int{1, 2, 3, 4, 5}
	fmt.Println(arr)
	fmt.Printf("origin array address: %p \n", &arr)
	passArray(arr)
	fmt.Println(arr)
}

func passArray (arr1 [5]int) {
	fmt.Printf("passed array address, arr1: %p \n", &arr1)
	fmt.Println(arr1)
	arr1[3] = 111
	fmt.Println("pass array arr1: ", arr1)
}
复制代码

执行结果以下

[1 2 3 4 5]
origin array address: 0xc000090000 
passed array address, arr1: 0xc000090060 
[1 2 3 4 5]
pass array arr1:  [1 2 3 111 5]
[1 2 3 4 5]
复制代码
  • 先打印该数组,没有问题
  • 在打印当前数组的地址为:0xc000090000
  • 再调用函数passArray,先打印改数组地址为:0xc000090060,能够看出这里的地址和原始数组的地址不同,这是由于这里传的是一个数组的副本,并不是指向原数组
  • 而后打印arr1数组,和原数组数据一致
  • 再更新角标为3的元素值为111,打印后的结果为:[1 2 3 111 5]。能够发现arr1数组已经更新了
  • 调用完成passArray后,在打印原始数组,发现数据仍为:[1 2 3 4 5]并无由于arr1的更新而受影响。

这是由于,在调用函数passArray时,传的是arr数组的一个副本,从新开辟了一个新空间存储这5个数组元素,不一样内存空间的数组变更是不会影响另外一块存储数组元素的内存空间的。

这种数组传递是很是笨重的,由于须要从新开辟一块空间把原来的数组copy一份,这里是5个元素,若是是1000或者10000个元素呢?因此,咱们能够经过其余的方式规避这种笨重的操做,没错,就是指针,代码以下

package main

import "fmt"

func main() {
	var arr = [5]int{1, 2, 3, 4, 5}
	fmt.Println(arr)
	fmt.Printf("origin array address: %p \n", &arr)
	passAddress(&arr)
	fmt.Println(arr)
}

func passAddress (arr2 *[5]int) {
	fmt.Printf("passed array address, arr2: %p \n", arr2)
	fmt.Printf("passed array address, arr2: %p \n", &arr2)
	fmt.Println(arr2)
	arr2[3] = 111
	fmt.Println("pass array arr2: ", *arr2)
}
复制代码

执行结果以下

[1 2 3 4 5]
origin array address: 0xc000084000 
passed array address, arr2: 0xc000084000 
passed array address, arr2: 0xc00000e010 
&[1 2 3 4 5]
pass array arr2:  [1 2 3 111 5]
[1 2 3 111 5]
复制代码
  • 先打印该数组,没有问题
  • 在打印当前数组的地址为:0xc000084000
  • 而后调用函数passAddress,注意这里传的是数组的地址,接收的是一个指针类型变量arr2。第一次咱们直接打印arr2,获得地址为:0xc000084000。没错,这里的意思是arr2这个指针指向的内存地址就是0xc000084000,即和原始数组指向的是同一块内存区域,也就是指向同一块存储这5个元素的区域。
  • 紧接着,打印arr2的地址,这个&arr2的意思是arr2这个指针的地址,为0xc00000e010,经过上面一点,咱们已经知道这个指针指向的地址是0xc000084000
  • 而后咱们打印arr2,获得&[1 2 3 4 5]
  • 以后咱们再改变第三个角标的值为111,并打印arr2指针指向的数组的值为:[1 2 3 111 5],即arr2中元素已经更新
  • 调用完passAddress后,咱们再次打印原始数组,获得的是:[1 2 3 111 5]

原始数组的值被改变了,这是由于咱们传递的是一个引用,经过一个地址指向了原来数组存储的地址。因此在函数passAddress中其实是对原来的内存空间的数据更新,显然也会反应到原来的数组上。

如上是数组传值的例子,slice和map也是传值的。虽然咱们在传递slice或者map的时候没有显式使用指针,可是他们的内部结构都间接使用了指针,因此slice和map都是引用类型,传递的时候至关于传递的是指针的副本,能够理解为上面数组中传指针的例子。

我的公众号:JackieZheng 我会将我最新的文章推送给您,并和您一块儿分享我平常阅读过的优质文章

相关文章
相关标签/搜索