首页 > 其他分享 >Go从入门到放弃之数组、切片

Go从入门到放弃之数组、切片

时间:2024-09-25 17:51:59浏览次数:9  
标签:入门 int fmt 元素 切片 Println 数组 Go

一、数组

数组的声明和初始化

在 Go 语言中,数组是固定长度的、同一类型的数据集合。数组中包含的每个数据项被称为数组元素,一个数组包含的元素个数被称为数组的长度。

在 Go 语言中,你可以通过 [] 来标识数组类型,但需要指定长度和元素类型,使用时可以修改数组成员,但是数组大小不可变化。以下是一些常见的数组声明方法:

var a [8]byte // 长度为8的数组,每个元素为一个字节
var b [3][3]int // 二维数组(9宫格)
var c [3][3][3]float64 // 三维数组(立体的9宫格)
var d = [3]int{1, 2, 3}  // 声明时初始化
var e = new([3]string)   // 通过 new 初始化

和普通变量赋值一样,数组也可以通过 := 进行一次性声明和初始化,所有数组元素通过 {} 包裹,然后通过逗号分隔多个元素

a := [5]int{1,2,3,4,5}

语法糖省略数组长度的声明

a := [...]int{1, 2, 3}

数组在初始化的时候,如果没有填满,则空位会通过对应的元素类型零值填充

a := [5]int{1, 2, 3}
fmt.Println(a)

//上述代码的打印结果是:
[1 2 3 0 0]

我们还可以初始化指定下标位置的元素值,未设置的位置也会以对应元素类型的零值填充

a := [5]int{1: 3, 3: 7}

//这样数组 a 的元素值如下:
[0 3 0 7 0]

数组的长度是该数组类型的一个内置常量,可以用 Go 语言的内置函数 len() 来获取

arrLength := len(arr)

数组元素的访问和设置

可以使用数组下标来访问 Go 数组中的元素,数组下标默认从 0 开始,len(arr)-1 表示最后一个元素的下标:

arr := [5]int{1,2,3,4,5}
a1, a2 := arr[0], arr[len(arr) - 1]
// 还可以通过下标设置对应索引位置的元素值:
arr[0] = 100

遍历数组

遍历数组a有以下两种方法

func main() {
	var a = [...]string{"北京", "上海", "深圳"}
	// 方法1:for循环遍历
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}

	// 方法2:for range遍历
	for index, value := range a {
		fmt.Println(index, value)
	}
}

如果我们不想获取索引值,可以这么做:

for _, v := range arr {
   // ...
}

如果只想获取索引值,可以这么做:

for i := range arr {
   // ...
}

多维数组

这里以二维数组为例(数组中又嵌套数组)

二维数组的定义

func main() {
	a := [3][2]string{
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}
	fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
	fmt.Println(a[2][1]) //支持索引取值:重庆
}

二维数组的遍历

func main() {
	a := [3][2]string{
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}
	for _, v1 := range a {
		for _, v2 := range v1 {
			fmt.Printf("%s\t", v2)
		}
		fmt.Println()
	}
}

注意: 多维数组只有第一层可以使用...来让编译器推导数组长度。例如:

//支持的写法
a := [...][2]string{
	{"北京", "上海"},
	{"广州", "深圳"},
	{"成都", "重庆"},
}
//不支持多维数组的内层使用...
b := [3][...]string{
	{"北京", "上海"},
	{"广州", "深圳"},
	{"成都", "重庆"},
}

数组是值类型

数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。

func modifyArray(x [3]int) {
	x[0] = 100
}

func modifyArray2(x [3][2]int) {
	x[2][0] = 100
}
func main() {
	a := [3]int{10, 20, 30}
	modifyArray(a) //在modify中修改的是a的副本x
	fmt.Println(a) //[10 20 30]
	b := [3][2]int{
		{1, 1},
		{1, 1},
		{1, 1},
	}
	modifyArray2(b) //在modify中修改的是b的副本x
	fmt.Println(b)  //[[1 1] [1 1] [1 1]]
}

数组类型的不足

由于数组类型变量一旦声明后长度就固定了,这意味着我们将不能动态添加元素到数组,如果要这么做的话,需要先创建一个容量更大的数组,然后把老数组的元素都拷贝过来,最后再添加新的元素,如果数组尺寸很大的话,势必会影响程序性能,例如

[3]int

另外,数组是值类型,这意味着作为参数传递到函数时,传递的是数组的值拷贝,也就是说,会先将数组拷贝给形参,然后在函数体中引用的是形参而不是原来的数组,当我们在函数中对数组元素进行修改时,并不会影响原来的数组,

这种机制带来的另一个负面影响是当数组很大时,值拷贝会降低程序性能。综合以上因素,我们迫切需要一个引用类型的、支持动态添加元素的新「数组」类型,这就是下篇教程将要介绍的切片类型,实际上,我们在 Go 语言中很少使用数组,大多数时候会使用切片取代它。

二、切片

在 Go 语言中,切片是一个新的数据类型,与数组最大的不同在于,切片的类型字面量中只有元素的类型,没有长度

Go从入门到放弃之数组、切片_数组

切片定义

声明切片类型的基本语法如下

// name:表示变量名
// T:表示切片中的元素类型

var name []T

创建切片的方法主要有三种 —— 基于数组、切片和直接创建  

基于数组

切片可以基于一个已存在的数组创建。从这个层面来说,数组可以看作是切片的底层数组,而切片则可以看作是数组某个连续片段的引用。切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的切片:

// 先定义一个数组
months := [...]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}

// 基于数组创建切片
q2 := months[3:6]    // 第二季度
summer := months[5:8]  // 夏季

fmt.Println(q2)
fmt.Println(summer)  

// 运行结果为:

[April May June]
[June July August]

Go 语言支持通过 array[start:end] 这样的方式基于数组生成一个切片,start 表示切片在数组中的下标起点,end 表示切片在数组中的下标终点,两者之间的元素就是切片初始化后的元素集合,通过上面的示例可以看到,和字符串切片一样,这也是个左闭右开的集合,下面几种用法也都是合法的:

// 基于 months 的所有元素创建切片(全年)
all := months[:]

// 基于 months 的前 6 个元素创建切片(上半年)
firsthalf := months[:6]

// 基于从第 6 个元素开始的后续元素创建切片(下半年)
secondhalf := months[6:]

基于切片

firsthalf := months[:6]
q1 := firsthalf[:3] // 基于 firsthalf 的前 3 个元素构建新切片
q1 := firsthalf[:9]

打印结果是:[January February March April May June July August September]。
因为 firsthalf 的容量是 12,只要选择的范围不超过 firsthalf 的容量,那么这个创建操作就是合法的,所以虽然是基于切片创建切片,但本质上还是基于数组。

使用make()函数构造切片

T:切片的元素类型
size:切片中元素的数量
cap:切片的容量

make([]T, size, cap)

示例:

func main() {
	a := make([]int, 2, 10)
	fmt.Println(a)      //[0 0]
	fmt.Println(len(a)) //2
	fmt.Println(cap(a)) //10
}

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量

判断切片是否为空

要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。

切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意

func main() {
	s1 := make([]int, 3) //[0 0 0]
	s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
	s2[0] = 100
	fmt.Println(s1) //[100 0 0]
	fmt.Println(s2) //[100 0 0]
}

切片遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历

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

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

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

动态增加元素

Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)

func main(){
	var s []int
	s = append(s, 1)        // [1]
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7]
}
// 注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。
var s []int
s = append(s, 1, 2, 3

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

举个例子:

func main() {
	//append()添加元素和切片扩容
	var numSlice []int
	for i := 0; i < 10; i++ {
		numSlice = append(numSlice, i)
		fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
	}
}

append()函数还支持一次性追加多个元素。 例如:

var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]

切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:  

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

从上面的代码可以看出以下内容:

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

使用copy()函数复制切片

首先我们来看一个问题:

func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(b) //[1 2 3 4 5]
	b[0] = 1000
	fmt.Println(a) //[1000 2 3 4 5]
	fmt.Println(b) //[1000 2 3 4 5]
}

由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下

srcSlice: 数据来源切片
destSlice: 目标切片

copy(destSlice, srcSlice []T)

示例:

func main() {
	// copy()复制切片
	a := []int{1, 2, 3, 4, 5}
	c := make([]int, 5, 5)
	copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1 2 3 4 5]
	c[0] = 1000
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1000 2 3 4 5]
}

从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素

func main() {
	// 从切片中删除元素
	a := []int{30, 31, 32, 33, 34, 35, 36, 37}
	// 要删除索引为2的元素
	a = append(a[:2], a[3:]...)
	fmt.Println(a) //[30 31 33 34 35 36 37]
}

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

  

"一劳永逸" 的话,有是有的,而 "一劳永逸" 的事却极少



标签:入门,int,fmt,元素,切片,Println,数组,Go
From: https://blog.51cto.com/u_8901540/12111299

相关文章

  • Go从入门到放弃之数据类型
    数据类型概述Go支持的数据类型基本数据类型布尔类型:bool整型:int8、byte、int16、int、uint、uintptr等浮点类型:float32、float64复数类型:complex64、complex128字符串:string字符类型:rune错误类型:error复合类型指针(pointer)数组(arr......
  • Go从入门到放弃之指针
    要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值指针地址和指针类型每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。Go语言中的值类型(int、float、bool、string、array、struct)都......
  • 2024-09-25:用go语言,给定一个长度为 n 的整数数组 nums 和一个正整数 k, 定义数组的“能
    2024-09-25:用go语言,给定一个长度为n的整数数组nums和一个正整数k,定义数组的"能量"为所有和为k的子序列的数量之和。请计算nums数组中所有子序列的能量和,并对结果取模10^9+7后返回。输入:nums=[1,2,3],k=3。输出:6。解释:总共有5个能量不为0的子序列:子......
  • 入门网络安全工程师要学习哪些内容
       大家都知道网络安全行业很火,这个行业因为国家政策趋势正在大力发展,大有可为!但很多人对网络安全工程师还是不了解,不知道网络安全工程师需要学什么?知了堂小编总结出以下要点。网络安全工程师是一个概称,学习的东西很多,具体学什么看自己以后的职业定位。如果你以后想成为......
  • 入门网络安全工程师要学习哪些内容
       大家都知道网络安全行业很火,这个行业因为国家政策趋势正在大力发展,大有可为!但很多人对网络安全工程师还是不了解,不知道网络安全工程师需要学什么?知了堂小编总结出以下要点。网络安全工程师是一个概称,学习的东西很多,具体学什么看自己以后的职业定位。如果你以后想成为......
  • Linux 基础入门操作 第十章 多线程实现
    10线程介绍线程是进程的一条执行路径。每个线程共享其所附属的进程的所有的资源,包括打开的文件、页表(因此也就共享整个用户态地址空间)、信号标识及动态分配的内存等等。线程和进程的关系是:线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一物理内存空间......
  • node/expressjs 连接与操作 MongoDB
    MongoDB 的安装、配置、启动、常见指令等,详见上一节“mongoDB简介” 以下将讲述 node/expressjs 与 mongoDB 的交互——连接与操作数据库 mongoDB注释:以下示例是采用express官网的生成器初始化项目的。数据库 mongoDB的操作运用的是mongoose插件, mong......
  • CTF入门教程(非常详细)从零基础入门到竞赛,看这一篇就够了!
        一、CTF简介CTF(CaptureTheFlag)中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式。CTF起源于1996年DEFCON全球黑客大会,以代替之前黑客们通过互相发起真实攻击进行技术比拼的方式。发展至今,已经成为全球范围网络安全圈流行......
  • mongoDB 简介
    参考文档 https://www.runoob.com/mongodb/mongodb-tutorial.htmlmongoDB菜鸟教程https://mongodb.net.cn/manual/mongoDB中文官网 1. mongoDB 概述MongoDB是一个流行的开源文档型数据库,它使用类似JSON的文档模型存储数据,这使得数据......
  • CTF入门教程(非常详细)从零基础入门到竞赛,看这一篇就够了!
        一、CTF简介CTF(CaptureTheFlag)中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式。CTF起源于1996年DEFCON全球黑客大会,以代替之前黑客们通过互相发起真实攻击进行技术比拼的方式。发展至今,已经成为全球范围网络安全圈流行......