首页 > 其他分享 >Go语言之切片(slice)快速入门篇

Go语言之切片(slice)快速入门篇

时间:2024-07-25 23:52:38浏览次数:9  
标签:slice s2 fmt cap len 切片 入门篇 Go s1

                                              作者:尹正杰

版权声明:原创作品,谢绝转载!否则将追究法律责任。

目录

一.切片(slice)概述

1.数组的局限性

数组的三个特点:
	- 1.长度固定;
	- 2.连续内存空间;
	- 3.同一类型集合;

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,比如数组(array)无法实现扩容和缩容。

2.切片(slice)概述


切片(slice)是Golang中一种特有的数据类型,如上图所示, 切片的本质就是对底层数组的封装,它包含了三个信息:
	- 1.底层数组的指针;
	- 2.切片的长度(len);
	- 3.切片的容量(cap);
	
	
切片是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。支持自动扩容。切片的三个特点:
	- 1.长度可变;
	- 2.连续内存空间;
	- 3.同一类型集合;

切片是数组一个连续片段的引用,所以切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

这个片段可以是整个数组,或者由起始和终止索引标识符的一些项的子集,终止索引标识的项不包括在切片内。

	
切片和数组的区别:
	- 1.切片长度不固定,可以根据需求自动扩容,数组长度是固定的;
	- 2.数组的长度和容量相等切不可变,而切片的长度和容量并不一定相等;

3.切片的内存分析

package main

import "fmt"

func main() {
	// 定义数组
	var intArray [5]uint8 = [5]uint8{1, 3, 5, 7, 9}

	// 切片构建在数组之上,如果基于数组的索引取切片一定要注意口诀: "前包后不包"。
	var slice []uint8 = intArray[1:4]

	fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))

	fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))

	fmt.Printf("intArray[1]数组的内存地址: %p\n", &intArray[1])

	fmt.Printf("slice[0]切片的内存地址: %p\n", &slice[0])
	fmt.Printf("slice[1]切片的内存地址: %p\n", &slice[1])

	// 修改切片的数据
	slice[1] = 88

	// 查看数组和切片的数据是否修改
	fmt.Printf("intArray数组: %v, 长度: %d, 容量: %d\n", intArray, len(intArray), cap(intArray))
	fmt.Printf("slice切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))

}

二.切片的三种定义方式

1.切片表达式(基于已经存在的数组来创建切片)

package main

import "fmt"

func main() {

	/*
			切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。

			切片表达式有两种变体:(省略了low则默认为0,省略了high则默认为切片操作数的长度)
		    	(1)一种指定low和high两个索引界限值的简单的形式;
					array[low:high]
		    	(2)另一种是除了low和high索引界限值外还指定容量的完整的形式;
					array[low:high:max]

			完整切片表达式需要满足的条件是0 >= low >= high >= max >= cap(a),其他条件和简单切片表达式相同;

			当设置了max时,则切片的容量设置为"max-low";
	*/

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

	s1 := a[2:] // 等同于 a[2:len(a)]
	s2 := a[:3] // 等同于 a[0:3]
	s3 := a[:]  // 等同于 a[0:len(a)]

	s4 := a[1:3:5]
	s5 := a[1:3:3]

	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
	fmt.Printf("s4:%v len(s4):%v cap(s4):%v\n", s4, len(s4), cap(s4))
	fmt.Printf("s5:%v len(s5):%v cap(s5):%v\n", s5, len(s5), cap(s5))

}

2.通过make指令创建切片

package main

import "fmt"

func main() {
	/*
		如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:
			make([]T, size, cap)

		通过make函数常见切片需要传入三个参数:
			T:
				切片的类型
			size:
				切片的长度
			cap:
				切片的容量,容量并不会影响当前元素的个数。 

		make创建切片本质上就是在底层创建了一个数组,该数组对外不可见,所以不可以直接操作这个数组,要通过切片去间接的访问各个元素。
	*/
	slice := make([]int, 4, 20)

	// 为切片赋值
	slice[1] = 100
	slice[3] = 200

	fmt.Printf("切片的长度:%d,容量:%d,数据:%v\n", len(slice), cap(slice), slice)

}

3.声明切片类型

package main

import (
	"fmt"
)

func main() {
	// 声明切片类型,定义一个切片,直接就指定具体数组,使用原理类似make
	var (
		// 声明一个字符串切片
		bigdata = []string{"hadoop", "spark", "flink", "kudu", "hbase", "hive"}

		// 声明一个整型切片并初始化
		scores = []int{99, 88, 77}

		// 声明一个布尔切片并初始化
		svip = []bool{false, true}
	)

	fmt.Printf("bigdata切片的长度:[%d],容量:[%d],数据:%v\n", len(bigdata), cap(bigdata), bigdata)
	fmt.Printf("scores切片的长度:[%d],容量:[%d],数据:%v\n", len(scores), cap(scores), scores)
	fmt.Printf("svip切片的长度:[%d],容量:[%d],数据:%v\n", len(svip), cap(svip), svip)
}

三.切片的遍历

1.基于for循环遍历

package main

import (
	"fmt"
)

func main() {
	s := []byte{'A', 'B', 'C'}

	// 支持基于索引遍历
	for i := 0; i < len(s); i++ {
		fmt.Printf("第[%d]个索引存储的数据是: [%c]\n", i, s[i])
	}
}

2.基于for-range循环遍历

package main

import (
	"fmt"
)

func main() {
	s := []byte{'A', 'B', 'C'}

	// 支持基for-range循环
	for index, value := range s {
		fmt.Printf("第[%d]个索引对应的数据为:%c\n", index, value)
	}
}

四.切片的扩容

1.通过append函数扩容切片

package main

import "fmt"

func main() {
	var (
		// 1.定义数组

		intArr [5]int = [5]int{1, 3, 5, 7, 9}
		// 2.定义切片

		s1 []int = intArr[1:4]
	)

	/*
		切片扩容的底层原理:
			- 1.底层追加元素的时候对数组进行扩容,老数组扩容为新数组;
			- 2.创建一个新数组,将老数组中的s1("3","5","7")复制到新数组中,在新数组中追加"66","88";
			- 3.s2底层数组指向的是新数组;
			- 4.往往我们在使用追加的时候起始想要做的效果给s1追加;
			- 5.底层的新数组不能直接维护,需要通过切片简洁维护操作;
	*/
	s2 := append(s1, 66, 88)

	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))

	s3 := []int{22, 33, 44}

	// append也支持将一个切片直接追加到另一个切片中
	s1 = append(s1, s3...)

	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))
	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))

}

2.切片自动扩容

package main

import (
	"fmt"
)

func main() {

	var numSlice []int
	fmt.Printf("未添加任何元素:%p 数据: %v  长度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))

	for i := 0; i < 10; i++ {
		/*
			温馨提示:
				- 1.每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。
				- 2.当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。
				- 3.“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
		*/
		numSlice = append(numSlice, i)

		fmt.Printf("内存地址指针:%p 数据: %v  长度:%d  容量:%d  \n", numSlice, numSlice, len(numSlice), cap(numSlice))
	}
}

3.切片的扩容策略[了解即可]

可以通过查看$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
            }
        }
    }


从上面的代码可以看出以下内容:
	- 1.首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap),否则走else语句继续判断;
	
	- 2.如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),否则走else语句继续判断;
	
	- 3.如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap);
	
	- 5.如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap);



温馨提示:
	切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。 

五.切片使用注意事项

1.切片使用注意事项

- 1.切片定义后不可以直接使用,需要让其引用到一个数组,或者make一个空间供切片来使用;

- 2.切片使用不能越界;

- 3.切片表达式支持简写形式
	案例1: 
		"var s1 = arr[0:end]"简写为:"var s1 = arr[:end]"
		
	案例2:
		"var s2 = arr[start:len(arr)]"简写为"var s2 = arr[start:]"
		
	案例3:
		"var s3 = arr[0:len(arr)]简写为"var s3 = arr[:]"
		
- 4.切片可以继续切片;

- 5.切片可以动态增长,通过"append"函数操作即可;

- 6.切片也支持使用内置函数copy进行拷贝;

- 7.切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较

1.切片的赋值拷贝

package main

import (
	"fmt"
)

func main() {
	s1 := make([]int, 3)

	// 切片是引用类型,将s1直接复制给s2,此时s1和s2底层共用同一个数组
	s2 := s1

	fmt.Printf("修改前: s1 --->[%v]\n", s1)
	fmt.Printf("修改前: s2 --->[%v]\n", s2)

	// 注意,我修改的是s2,并没有取修改s1哈
	s2[1] = 100

	// 再次查看s1和s2时你会发现二者都发生了变化哟
	fmt.Printf("修改后: s1 --->[%v]\n", s1)
	fmt.Printf("修改后: s2 --->[%v]\n", s2)

}

2.切片的复制

package main

import (
	"fmt"
)

func main() {
	s1 := []int{11, 22, 33, 44, 55}
	s2 := s1

	// 切片是引用类型,所以s1和s2其实都指向了同一块内存地址。修改s2的同时s1的值也会发生变化。
	s2[1] = 100
	fmt.Printf("修改前: s1 ---> 内存地址: %p [%v], \n", s1, s1)
	fmt.Printf("修改前: s2 ---> 内存地址: %p [%v]\n", s2, s2)

	// 注意,使用make函数对切片进行初始化操作,此操作在"go1.19.3"版本中对s1和s2指向的数组值也有影响哟~
	s3 := make([]int, 3)

	// Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
	// 		copy(destSlice, srcSlice []T)
	//
	// 温馨提示:
	//		srcSlice:
	//			数据来源切片
	// 		destSlice:
	//			目标切片
	copy(s1, s3) // 使用copy()函数将切片s1中的元素复制到切片s3,属于值拷贝。

	// 由于s3属于只是使用了内置函数copy对s1进行了值拷贝,因此修改s3并不会影响到s1和s2哟~
	s3[2] = 200
	fmt.Printf("s1 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s1, s1, len(s1), cap(s1))
	fmt.Printf("s2 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s2, s2, len(s2), cap(s2))
	fmt.Printf("s3 ---> 内存地址: %p 数据: %v 长度: %d 容量: %d\n", s3, s3, len(s3), cap(s3))
}

3.切片元素删除

package main

import (
	"fmt"
)

func main() {

	numberList := []int{11, 22, 33, 44, 55}
	fmt.Printf("删除前 ---> numberList: %v\n", numberList)

	// Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
	// 要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)
	numberList = append(numberList[:3], numberList[4:]...) // 删除索引为3的元素

	fmt.Printf("删除后 ---> numberList: %v\n", numberList)
}

4.切片不能直接比较

package main

import (
	"fmt"
)

func main() {
	var a = []bool{false, true}
	var b = []bool{false, true}

	fmt.Printf("a ---> %v\n", a)
	fmt.Printf("b ---> %v\n", b)

	// 切片是引用类型,不支持直接比较,切片唯一合法的比较操作是和nil比较。
	// fmt.Println(a == b) //  报错: invalid operation: a == b (slice can only be compared to nil)

}

5.判断切片是否为空

package main

import (
	"fmt"
)

func main() {
	var s1 []int
	s2 := []int{}
	s3 := make([]int, 0)
	fmt.Printf("s1:%v len(s1):%v cap(s1):%v\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
	fmt.Printf("s3:%v len(s3):%v cap(s3):%v\n", s3, len(s3), cap(s3))

	// 温馨提示:
	// 		(1)一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量一定都是0;
	// 		(2)我们不能说一个长度和容量都是0的切片一定是nil;
	fmt.Printf("s1是否为空: %t\n", s1 == nil)
	fmt.Printf("s2是否为空: %t\n", s2 == nil)
	fmt.Printf("s3是否为空: %t\n", s3 == nil)

	// 综上所述,要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。
	fmt.Println(len(s1) == 0)
	fmt.Println(len(s2) == 0)
	fmt.Println(len(s3) == 0)

}

六.练习题

1.观看代码手写运行结果

package main

import (
	"fmt"
)

func main() {
	var a = make([]string, 5, 10)
	fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))

	for i := 0; i < 10; i++ {
		a = append(a, fmt.Sprintf("%v", i))
	}

	fmt.Printf("数据: %v 长度: %d 容量: %d\n", a, len(a), cap(a))
}

2.使用sort包对数组进行排序

package main

import (
	"fmt"
	"sort"
)

func main() {

	var a = [...]int{3, 7, 8, 9, 1}
	fmt.Printf("排序前: %v\n", a)

	sort.Ints(a[:]) // 对切片进行排序,该切片底层对应的就是上面的可变数组哟~
	fmt.Printf("排序后: %v\n", a)
}

标签:slice,s2,fmt,cap,len,切片,入门篇,Go,s1
From: https://www.cnblogs.com/yinzhengjie/p/18324368

相关文章

  • go语言学习文档精简版
    Go语言是一门开源的编程语言,目的在于降低构建简单、可靠、高效软件的门槛。Go平衡了底层系统语言的能力,以及在现代语言中所见到的高级特性。你好,Gopackagemain//程序组织成包import"fmt"//fmt包用于格式化输出数据//主函数入口funcmain(){//输出语句......
  • django_创建菜单(实现整个项目的框架,调包)
    文章目录前言代码仓库地址在线演示网址启动网站的时候出现错误渲染路径的一些说明文件结构网页显示一条错误路由顺序js打包出现问题的代码函数没有起作用关于进度开发细节显示不了图片梳理一下函数调用的流程修改一些宽度参数classjs里面的一些细节让三个按钮可以点击设......
  • go和Spex编译分析
    为什么会报错某个GitHub文件被排除了呢 可能这个文件本身有+buildignore字段 go.mod类似于idea里的pom文件用于管理依赖dep   Gopkg.lock文件 为什么清楚依赖缓存会有用 引入代码版本v不是必须的,而是根据项目中有没有加v 私有受保护仓库......
  • Django学习(一)
    Django起源:项目创建:项目执行(测试开发阶段): 项目结束: 结构:db.sqlite3:数据库,一般不用,会替换成我们自己的数据库manage.py 项目同名目录: settings.py settings中的配置详解:   注意:添加自定义配置时名称必须大写 django处理url的过程 视图函数 ......
  • 【golang设计模式】—— 简单工厂模式
    模式定义简单工厂模式(SimpleFactoryPattern):又称为静态工厂方法(StaticFactoryMethod)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。模式结构工......
  • 全链路追踪 & 性能监控,GO 应用可观测全面升级
    作者:古琦01介绍随着Kubernetes和容器化技术的普及,Go语言不仅在云原生基础组件领域广泛应用,也在各类业务场景中占据了重要地位。如今,越来越多的新兴业务选择Golang作为首选编程语言。得益于丰富的RPC框架(如Gin、Kratos、Kitex等),Golang在微服务生态中愈加成熟,并被用于很......
  • Django 管理员:一对一关系作为内联?
    我正在为satchmo应用程序整理管理员。Satchmo使用OneToOne关系来扩展基本模型Product,我想在一页上编辑所有内容。是否可以将OneToOne关系作为内联关系?如果没有,将一些字段添加到管理员的给定页面并最终保存到OneToOne关系中的最佳方法是什么?例如:clas......
  • ubuntu下goland打开新的项目闪退的解决办法
    安装最新的ubuntu2024.04版本的desktop,安装了goland作go的开发遇到问题,刚从服务器clone的项目,使用goland打开,会闪退,再打开goland,会回到上一次正常打开的项目经过多次测试,发现是无法自动创建.idea目录导致,我复制一个其他项目的.idea的目录进取后,可以正常打开,但相关项目是信息是错......
  • Django 应用程序部署到 url 子目录下
    在我的服务器中,django和nginx部署在ECSFargate上并连接到负载均衡器,但是URL是由Akamai传输的https://www.example.com/company/playground/*->https://amazonloadbalancer/*但是,出现了一些问题,例如问题1静态访问https://www.exmplae.com/company/play......
  • 使用pymongo连接mongodb数据库与在mongodb数据库中查询数据
            MongoDB是一个开源的文档型数据库管理系统,采用BSON(BinaryJSON)格式存储数据。它以其灵活的数据模型、强大的查询语言和高性能的数据读写能力而广受欢迎。MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富、最像关系数据库的......