Go语言之切片

简介:

切片也是一种数据结构,它和数组非常相似,因为他是围绕动态数组的概念设计的,可以按需自动改变大小,使用这种结构,可以更方便地管理和使用数据集合。


内部实现


切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为机遇数组实现,所以它的底层的内存是连续非配的,效率非常高。它还有可以通过索引获得数据、可以迭代以及垃圾回收优化的好处。


切片对象非常小,是因为它是只有 3 个字段的数据结构:一个是指向底层数组的指针,一个是切片的长度,一个是切片的容量。这 3 个字段,就是Go语言操作底层数组的元数据,有了它们,我们就可以任意地操作切片了。


声明和初始化


切片创建的方式有好几种,我们先看下最简洁的make方式。


slice:=make([]int,5)


使用内置的make函数时,需要传入一个参数,指定切片的长度,例子中我们使用的时 5 ,这时候切片的容量也是 5 。当然我们也可以单独指定切片的容量。


slice:=make([]int,5,10)


这时,我们创建的切片长度是 5 ,容量是 10 。需要注意的这个容量 10 其实对应的是切片底层数组的。


因为切片的底层是数组,所以创建切片时,如果不指定字面值的话,默认值就是数组的元素的零值。这里我们所以指定了容量是 10 ,但是我们只能访问 5 个元素。因为切片的长度是 5 ,剩下的 5 个元素,需要切片扩充后才可以访问。


容量必须>=长度,我们是不能创建长度大于容量的切片的。


还有一种创建切片的方式,是使用字面量,就是指定初始化的值。


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


有没有发现,跟创建数组非常像,只不过不用制定[]中的值。这时候切片的长度和容量是相等的,并且会根据我们指定的字面量推导出来。当然我们也可以像数组一样,只初始化某个索引的值:


slice:=[]int{4:1}


这是指定了第 5 个元素为 1 ,其他元素都是默认值 0 。这时候切片的长度和容量也是一样的。这里再次强调一下切片和数组的微小差别。


//数组
array:=[5]int{4:1}
//切片
slice:=[]int{4:1}


切片还有nil切片和空切片,它们的长度和容量都是 0 。但是它们指向底层数组的指针不一样,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是个地址。


//nil切片
var nilSlice []int
//空切片
slice:=[]int{}


nil切片表示不存在的切片,而空切片表示一个空集合,它们各有用处。


切片另外一个用处比较多的创建是基于现有的数组或者切片创建。


slice := []int{1, 2, 3, 4, 5}
slice1 := slice[:]
slice2 := slice[0:]
slice3 := slice[:5]

fmt.Println(slice1)
fmt.Println(slice2)
fmt.Println(slice3)


基于现有的切片或者数组创建,使用[i:j]这样的操作符即可。它表示以i索引开始,到j索引结束,截取原数组或者切片,创建而成的新切片。新切片的值包含原切片的i索引,但是不包含j索引。对比Java的话,发现和String的subString方法很像。


i如果省略,默认是 0 ;j如果省略,默认是原数组或者切片的长度。所以例子中的三个新切片的值是一样的。这里注意的是ij都不能超过原切片或者数组的索引。


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

newSlice[0] = 10

fmt.Println(slice)
fmt.Println(newSlice)


这个例子证明了,新的切片和原切片共用的是一个底层数组。所以当修改的时候,底层数组的值就会被改变,所以原切片的值也改变了。当然对于基于数组的切片也一样的。


我们基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?这里有个公式:


对于底层数组容量是k的切片slice[i:j]来说
长度:j-i
容量:k-i


比如我们上面的例子slice[1:3],长度就是3-1=2,容量是5-1=4。不过代码中我们计算的时候不用这么麻烦,因为Go语言为我们提供了内置的lencap函数来计算切片的长度和容量。


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

fmt.Printf("newSlice长度:%d,容量:%d",len(newSlice),cap(newSlice))


以上是基于一个数组或者切片使用 2 个索引创建新切片的方法。此外还有一种 3 个索引的方法,第 3 个用来限定新切片的容量,其用法为slice[i:j:k]


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


这样我们就创建了一个长度为2-1=1,容量为3-1=2的新切片,不过第三个索引,不能超过原切片的最大索引值 5 。


使用切片


使用切片,和使用数组一样,通过索引就可以获取切片对应元素的值,同样也可以修改对应元素的值。


slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice[2]) //获取值
slice[2] = 10 //修改值
fmt.Println(slice[2]) //输出10


切片只能访问到其长度内的元素,访问超过长度外的元素,会导致运行时异常,与切片容量关联的元素只能用于切片增长。


我们前面讲了,切片算是一个动态数组,所以它可以按需增长,我们使用内置append函数即可。append函数可以为一个切片追加一个元素,至于如何增加、返回的是原切片还是一个新切片、长度和容量如何改变这些细节,append函数都会帮我们自动处理。


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

newSlice=append(newSlice,10)
fmt.Println(newSlice)
fmt.Println(slice)

//Output
[2 3 10]
[1 2 3 10 5]


例子中,通过append函数为新创建的切片newSlice,追加了一个元素 10 。我们发现打印的输出,原切片slice的第 4 个值也被改变了,变成了 10 。引起这种结果的原因是因为newSlice有可用的容量,不会创建新的切片来满足追加,所以直接在newSlice后追加了一个元素 10 。因为newSliceslice切片共用一个底层数组,所以切片slice的对应的元素值也被改变了。


这里newSlice新追加的第 3 个元素,其实对应的是slice的第 4 个元素,所以这里的追加其实是把底层数组的第4个元素修改为 10 ,然后把newSlice长度调整为 3 。


如果切片的底层数组没有足够的容量时,就会新建一个底层数组,把原来数组的值复制到新底层数组里,再追加新值,这时候就不会影响原来的底层数组了。


所以一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片。


append函数会智能地增长底层数组的容量,目前的算法是:容量小于 1000 个时,总是成倍的增长;一旦容量超过 1000 个,增长因子设为 1.25 ,也就是说每次会增加 25% 的容量。


内置的append也是一个可变参数的函数,所以我们可以同时追加好几个值。


newSlice=append(newSlice,10,20,30)


此外,我们还可以通过...操作符,把一个切片追加到另一个切片里。


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

newSlice=append(newSlice,slice...)
fmt.Println(newSlice)
fmt.Println(slice)


迭代切片


切片是一个集合,我们可以使用for range循环来迭代它,打印其中的每个元素以及对应的索引。


    slice := []int{1, 2, 3, 4, 5}
    for i,v:=range slice{
        fmt.Printf("索引:%d,值:%d\n",i,v)
    }


如果我们不想要索引,可以使用_来忽略它。这是Go语言的用法,很多不需要的函数等返回值,都可以忽略。


    slice := []int{1, 2, 3, 4, 5}
    for _,v:=range slice{
        fmt.Printf("值:%d\n",v)
    }


这里需要说明的是range返回的是切片元素的复制,而不是元素的引用。


除了for range循环外,我们也可以使用传统的for循环,配合内置的len函数进行迭代。


 

    slice := []int{1, 2, 3, 4, 5}
    for i := 0; i < len(slice); i++ {
        fmt.Printf("值:%d\n", slice[i])
    }

在函数间传递切片


我们知道切片是 3 个字段构成的结构类型,所以在函数间以值的方式传递的时候,占用的内存非常小,成本很低。在传递复制切片的时候,其底层数组不会被复制,也不会受影响,复制只是复制的切片本身,不涉及底层数组。


func main() {
    slice := []int{1, 2, 3, 4, 5}
    fmt.Printf("%p\n", &slice)
    modify(slice)
    fmt.Println(slice)
}

func modify(slice []int) {
    fmt.Printf("%p\n", &slice)
    slice[1] = 10
}


打印的输出如下:


0xc420082060
0xc420082080
[1 10 3 4 5]


仔细看,这两个切片的地址不一样,所以可以确认切片在函数间传递是复制的。而我们修改一个索引的值后,发现原切片的值也被修改了,说明它们共用一个底层数组。


在函数间传递切片非常高效,而且不需要传递指针和处理复杂的语法,只需要复制切片,然后根据自己的业务修改,最后传递回一个新的切片副本即可。这也是为什么函数间使用切片传递参数,而不是数组的原因。


关于多维切片就不介绍了,还有多维数组,一来它和普通的切片数组一样,只不过是多个一维组成的多维;二来我压根不推荐用多维切片和数组,可读性不好,结构不够清晰,容易出问题。



本文转自 baby神 51CTO博客,原文链接:http://blog.51cto.com/babyshen/1913253,如需转载请自行联系原作者

相关文章
|
8天前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
1天前
|
安全 Java Go
探索Go语言在高并发环境中的优势
在当今的技术环境中,高并发处理能力成为评估编程语言性能的关键因素之一。Go语言(Golang),作为Google开发的一种编程语言,以其独特的并发处理模型和高效的性能赢得了广泛关注。本文将深入探讨Go语言在高并发环境中的优势,尤其是其goroutine和channel机制如何简化并发编程,提升系统的响应速度和稳定性。通过具体的案例分析和性能对比,本文揭示了Go语言在实际应用中的高效性,并为开发者在选择合适技术栈时提供参考。
|
4天前
|
运维 Kubernetes Go
"解锁K8s二开新姿势!client-go:你不可不知的Go语言神器,让Kubernetes集群管理如虎添翼,秒变运维大神!"
【8月更文挑战第14天】随着云原生技术的发展,Kubernetes (K8s) 成为容器编排的首选。client-go作为K8s的官方Go语言客户端库,通过封装RESTful API,使开发者能便捷地管理集群资源,如Pods和服务。本文介绍client-go基本概念、使用方法及自定义操作。涵盖ClientSet、DynamicClient等客户端实现,以及lister、informer等组件,通过示例展示如何列出集群中的所有Pods。client-go的强大功能助力高效开发和运维。
22 1
|
5天前
|
SQL 关系型数据库 MySQL
Go语言中使用 sqlx 来操作 MySQL
Go语言因其高效的性能和简洁的语法而受到开发者们的欢迎。在开发过程中,数据库操作不可或缺。虽然Go的标准库提供了`database/sql`包支持数据库操作,但使用起来稍显复杂。为此,`sqlx`应运而生,作为`database/sql`的扩展库,它简化了许多常见的数据库任务。本文介绍如何使用`sqlx`包操作MySQL数据库,包括安装所需的包、连接数据库、创建表、插入/查询/更新/删除数据等操作,并展示了如何利用命名参数来进一步简化代码。通过`sqlx`,开发者可以更加高效且简洁地完成数据库交互任务。
12 1
|
10天前
|
XML JSON Go
微服务架构下的配置管理:Go 语言与 yaml 的完美结合
微服务架构下的配置管理:Go 语言与 yaml 的完美结合
|
5天前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
6天前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
7天前
|
安全 Go API
go语言中的Atomic操作与sema锁
在并发编程中,确保数据一致性和程序正确性是关键挑战。Go语言通过协程和通道提供强大支持,但在需精细控制资源访问时,Atomic操作和sema锁变得至关重要。Atomic操作确保多协程环境下对共享资源的访问是不可分割的,如`sync/atomic`包中的`AddInt32`等函数,底层利用硬件锁机制实现。sema锁(信号量锁)控制并发协程数量,其核心是一个uint32值,当大于零时通过CAS操作实现锁的获取与释放;当为零时,sema锁管理协程休眠队列。这两种机制共同保障了Go语言并发环境下的数据完整性和程序稳定性。
|
8天前
|
算法 Go
Go 语言 实现冒泡排序
冒泡排序是大家熟知的经典算法。在Go语言中实现它,关键在于理解其核心思想:通过不断比较并交换相邻元素,让序列中的最大值像泡泡一样“浮”至顶端。每一轮比较都能确定一个最大值的位置。外层循环控制排序轮数,内层循环负责比较与交换。随着每轮排序完成,未排序部分逐渐缩小,直至整个数组有序。以下是Go语言实现示例及说明。
16 1
|
10天前
|
设计模式 算法 测试技术
动态支付策略:Go 语言中策略模式的妙用
动态支付策略:Go 语言中策略模式的妙用