Go语言切片一网打尽,别再和Java语法傻傻分不清楚

简介: 我总想着搞清楚,什么样的技术文章才算是好的文章呢?因为写一篇今后自己还愿意阅读的文章并不容易,暂时只能以此为目标努力。

前言

我总想着搞清楚,什么样的技术文章才算是好的文章呢?因为写一篇今后自己还愿意阅读的文章并不容易,暂时只能以此为目标努力。

最近开始用Go刷一些题,遇到了一些切片相关的细节问题,这里做一些总结。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。

切片的结构

type slice struct {
    array unsafe.Pointer     // 一个指向底层数组的指针(切片中元素是存在这个指向的数组中)
    len int                                 // 切片的长度:包含的元素个数
    cap int                                 // 切片的容量,len <= cap,如果len == cap,则添加元素会触发切片的扩容
}

长度为3,容量为5的int切片的图示如下,此时切片数组中可访问的部分只有下标0,1,2,超过部分不能访问。

image-20220129100007594

声明和初始化

nil切片

声明nil切片,声明之后就会初始化(默认会用nil初始化),此时slice == nil成立,用于描述一个不存在的切片。

var slice []int    // 此时 slice == nil 成立

image-20220129100614622

空切片

声明并初始化空切片,表示一个空的集合,空切片指向地址不是nil。

slice := make([]int, 0) // 此时 slice == nil 不成立
slice := []int{}

image-20220129100934875

无论是nil切片还是空切片在调用内置函数append、len和cap的效果都是一样的。

含有元素的切片

此时切片非空。

slice := []int{1, 2, 3, 4, 5}     // 声明并初始化一个切片,len和cap都是5
slice := make([]int, 4)                    // 声明并初始化一个切片,第二个参数表示切片的长度len和容量cap都为4
slice := make([]int, 4, 6)             // 声明并初始化一个切片,第二个参数表示len,第三个表示cap
-----------------------------------------------------------
array := [4]int{1, 2, 3, 4}            
slice := array[1:2]                            // 对于array[i:j]来说,新切片的len=j-i,且cap=k-i(这里k是原数组的大小)

image-20220129105637373

测试上面第四种初始化切片的方法:

func main() {
    array := [...]int{1, 2, 3, 4}
    slice := array[1:2]
    fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
}
/*
输出:
0xc00000c030 1 3 [2]
*/

拷贝

使用 := 拷贝

注意:下面代码中newSlice切片是通过slice切片声明并初始化的,虽然两个切片打印的地址不同,但是切片的地址指针指向的数组是同一个。修改slice[0] = 100之后,newSlice[0]也变成100。这种规则适用于将切片作为参数传递给函数,在函数的内部使用的是传入切片的值拷贝(创建一块新的内存存放切片,但切片的地址指针指向的是同一个数组)

image-20220129111918283

func main() {
    array := [...]int{1, 2, 3, 4}
    slice := array[1:2]
    newSlice := slice                // 拷贝,newSlice由一块新的内存存放slice切片信息
  /*
      上面这种初始化newSlice的写法等价于:
      var newSlice []int
      newSlice = slice
  */
    fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
    fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
    slice[0] = 100
    fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
    fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
}
/*
输出:
0xc00000c030 1 3 [2]
0xc00000c048 1 3 [2]
0xc00000c030 1 3 [100]
0xc00000c048 1 3 [100]
*/

使用copy函数拷贝

copy函数的两个参数是两个切片(将第二个切片的值覆盖到第一个切片),二者地址指针指向两个不同的数组。

func main() {
    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{5, 4, 3}
    fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
    fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
    //copy(slice2, slice1)                 // 只会复制slice1的前3个元素到slice2中
    copy(slice1, slice2)                     // 只会复制slice2的3个元素到slice1的前3个位置
    fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
    //fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
    slice2[0] = 200            
    slice1[0] = 100
    fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
    fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
}
/*
输出:
5 5 0xc00000c018 [1 2 3 4 5]
3 3 0xc00000c030 [5 4 3]
5 5 0xc00000c018 [5 4 3 4 5]
5 5 0xc00000c018 [100 4 3 4 5]        copy函数的两个切片的地址指针分别指向两个不同的数组(修改值互不影响)
3 3 0xc00000c030 [200 4 3]
*/

扩容

扩容使用append方法。

len == cap 时添加元素

func main() {
    slice := []int{1, 2, 3}            // 此时slice的len == cap == 3,append元素会触发扩容,扩容后切片地址指向新数组(具体扩容策略这里暂时不多深入)
    fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
    newSlice := append(slice, 1)
    fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
    slice[0] = 100
    fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
    fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
}
/*
输出:
0xc00000c030 3 3 [1 2 3]
0xc00000c060 4 6 [1 2 3 1]        // append一个元素之后,切片中指针指向一个扩容后新的数组(地址指针变化,这里打印出的是切片地址,无论是否扩容newSlice和slice内存地址必然是不同的,和二者拥有的地址指针不要搞混)
0xc00000c030 3 3 [100 2 3]        // 修改slice[0]的元素为100
0xc00000c060 4 6 [1 2 3 1]        // 但是newSlice[0]中的元素没有变化(这里才可以证明两个切片的地址指针指向两个不同的数组)
*/

len < cap 时添加元素

此时调用append方法添加一个元素1并不会创建新的数组,但是1会去覆盖掉array[2]。

image-20220129114640983

func main() {
    array := [4]int{1, 2, 3, 4}
    slice := array[0:2]
    fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
    newSlice := append(slice, 1)
    fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
    slice[0] = 100
    fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
    fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
  fmt.Println("array = ", array)
}
/*
输出:
0xc00000c030 2 4 [1 2]                // 这里看到从数组中初始化的slice的len==2且cap==4
0xc00000c060 3 4 [1 2 1]            // append之后,添加一个元素,len变为3(且append元素会覆盖底层数组,这里1覆盖了之前的3)
0xc00000c030 2 4 [100 2]            // 注意,这里slice打印出来的len还是2,slice的len和cap与newSlice的len和cap是隔离的,尽管它们地址指针指向的是同一个数组,这里修改了slice[0]=100
0xc00000c060 3 4 [100 2 1]        // newSlice[0]也被修改为100
array =  [100 2 1 4]                    // 证明指向的array数组也被修改了
*/

结束

快过年了,祝大家新年快乐,春招offer拿不停。

相关文章
|
6天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
4天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
13 2
|
4天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
15 2
|
8天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
84 58
|
7天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
4天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
12 4
|
4天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
17 1
|
8天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
8天前
|
Go 数据处理 调度
探索Go语言的并发模型:Goroutines与Channels的协同工作
在现代编程语言中,Go语言以其独特的并发模型脱颖而出。本文将深入探讨Go语言中的Goroutines和Channels,这两种机制如何协同工作以实现高效的并发处理。我们将通过实际代码示例,展示如何在Go程序中创建和管理Goroutines,以及如何使用Channels进行Goroutines之间的通信。此外,本文还将讨论在使用这些并发工具时可能遇到的常见问题及其解决方案,旨在为Go语言开发者提供一个全面的并发编程指南。
|
6天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。