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

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

阅读目录

回到顶部

一、数组

数组的声明和初始化

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

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

1

2

3

4

5

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 初始化

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

1

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

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

1

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

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

1

2

3

4

5

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

fmt.Println(a)

 

//上述代码的打印结果是:

[1 2 3 0 0]

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

1

2

3

4

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

 

//这样数组 a 的元素值如下:

[0 3 0 7 0]

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

1

arrLength := len(arr)

数组元素的访问和设置

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

1

2

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

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

遍历数组

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

1

2

3

4

5

6

7

8

9

10

11

12

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)

    }

}

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

1

2

3

for _, v := range arr {

   // ...

}

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

1

2

3

for i := range arr {

   // ...

}

多维数组

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

二维数组的定义

1

2

3

4

5

6

7

8

9

func main() {

    a := [3][2]string{

        {"北京""上海"},

        {"广州""深圳"},

        {"成都""重庆"},

    }

    fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]

    fmt.Println(a[2][1]) //支持索引取值:重庆

}

二维数组的遍历

1

2

3

4

5

6

7

8

9

10

11

12

13

func main() {

    a := [3][2]string{

        {"北京""上海"},

        {"广州""深圳"},

        {"成都""重庆"},

    }

    for _, v1 := range a {

        for _, v2 := range v1 {

            fmt.Printf("%s\t", v2)

        }

        fmt.Println()

    }

}

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

1

2

3

4

5

6

7

8

9

10

11

12

//支持的写法

a := [...][2]string{

    {"北京""上海"},

    {"广州""深圳"},

    {"成都""重庆"},

}

//不支持多维数组的内层使用...

b := [3][...]string{

    {"北京""上海"},

    {"广州""深圳"},

    {"成都""重庆"},

}

数组是值类型

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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]]

} 

数组类型的不足

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

1

2

3

4

5

6

7

//这个求和函数只能接受[3]int类型,其他的都不支持<br>func arraySum(x [3]int) int{

    sum := 0

    for _, v := range x{

        sum = sum + v

    }

    return sum

}

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

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

回到顶部

二、切片

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

切片定义

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

1

2

3

4

// name:表示变量名

// T:表示切片中的元素类型

 

var name []T

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

基于数组

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// 先定义一个数组

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 表示切片在数组中的下标终点,两者之间的元素就是切片初始化后的元素集合,通过上面的示例可以看到,和字符串切片一样,这也是个左闭右开的集合,下面几种用法也都是合法的:

1

2

3

4

5

6

7

8

// 基于 months 的所有元素创建切片(全年)

all := months[:]

 

// 基于 months 的前 6 个元素创建切片(上半年)

firsthalf := months[:6]

 

// 基于从第 6 个元素开始的后续元素创建切片(下半年)

secondhalf := months[6:]

基于切片

1

2

3

4

5

firsthalf := months[:6]

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

 

打印结果是:[January February March April May June July August September]。

因为 firsthalf 的容量是 12,只要选择的范围不超过 firsthalf 的容量,那么这个创建操作就是合法的,所以虽然是基于切片创建切片,但本质上还是基于数组。

使用make()函数构造切片

1

2

3

4

5

T:切片的元素类型

size:切片中元素的数量

cap:切片的容量

 

make([]T, size, cap) 

示例:

1

2

3

4

5

6

7

8

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来判断。

切片的赋值拷贝

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

1

2

3

4

5

6

7

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遍历

1

2

3

4

5

6

7

8

9

10

11

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()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)

1

2

3

4

5

6

7

8

9

10

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函数的返回值。

举个例子:

1

2

3

4

5

6

7

8

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()函数还支持一次性追加多个元素。 例如:

1

2

3

4

5

6

7

8

9

var citySlice []string

// 追加一个元素

citySlice = append(citySlice, "北京")

// 追加多个元素

citySlice = append(citySlice, "上海""广州""深圳")

// 追加切片

a := []string{"成都""重庆"}

citySlice = append(citySlice, a...)

fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]

切片的扩容策略

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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()函数复制切片

首先我们来看一个问题:

1

2

3

4

5

6

7

8

9

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()函数的使用格式如下

1

2

3

4

srcSlice: 数据来源切片

destSlice: 目标切片

 

copy(destSlice, srcSlice []T) 

示例:

1

2

3

4

5

6

7

8

9

10

11

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语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素

1

2

3

4

5

6

7

8

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]

}<br>

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


相关文章
|
2月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟蒋星熠Jaxonic,Go语言探索者。深耕云计算、微服务与并发编程,以代码为笔,在二进制星河中书写极客诗篇。分享Go核心原理、性能优化与实战架构,助力开发者掌握云原生时代利器。#Go语言 #并发编程 #性能优化
439 43
Go语言深度解析:从入门到精通的完整指南
|
6月前
|
存储 JSON 安全
Go语言切片,使用技巧与避坑指南
Go语言中的切片(Slice)是动态引用数组的高效数据结构,支持扩容与截取。本文从切片基础、常用操作到高级技巧全面解析,涵盖创建方式、`append`扩容机制、共享陷阱及安全复制等内容。通过代码示例详解切片特性,如预分配优化性能、区分`nil`与空切片、处理多维切片等。掌握这些核心知识点,可编写更高效的Go代码。
222 2
|
7月前
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
244 33
|
3月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟 蒋星熠Jaxonic,执着的星际旅人,用Go语言编写代码诗篇。🚀 Go语言以简洁、高效、并发为核心,助力云计算与微服务革新。📚 本文详解Go语法、并发模型、性能优化与实战案例,助你掌握现代编程精髓。🌌 从goroutine到channel,从内存优化到高并发架构,全面解析Go的强大力量。🔧 实战构建高性能Web服务,展现Go在云原生时代的无限可能。✨ 附技术对比、最佳实践与生态全景,带你踏上Go语言的星辰征途。#Go语言 #并发编程 #云原生 #性能优化
|
5月前
|
数据采集 机器学习/深度学习 存储
Go语言实战案例 - 找出切片中的最大值与最小值
本案例通过实现查找整数切片中的最大值与最小值,帮助初学者掌握遍历、比较和错误处理技巧,内容涵盖算法基础、应用场景及完整代码示例,适合初学者提升编程能力。
|
6月前
|
Go 索引
Go语言中使用切片需要注意什么?
本文详细讲解了Go语言中切片(Slice)的使用方法与注意事项。切片是对数组连续片段的引用,具有灵活的操作方式。文章从定义与初始化、长度与容量、自动扩容、共享底层数组、复制、边界检查、零值到拼接等方面展开,并配以示例代码演示。通过学习,读者可深入了解切片的工作原理及优化技巧,避免常见陷阱,提升编程效率与代码质量。
172 2
|
7月前
|
安全 Go 开发者
Go语言之切片的原理与用法 - 《Go语言实战指南》
切片(slice)是Go语言中用于处理变长数据集合的核心结构,基于数组的轻量级抽象,具有灵活高效的特点。切片本质是一个三元组:指向底层数组的指针、长度(len)和容量(cap)。本文详细介绍了切片的声明与初始化方式、基本操作(如访问、修改、遍历)、长度与容量的区别、自动扩容机制、共享与副本处理、引用类型特性以及常见陷阱。通过理解切片的底层原理,开发者可以更高效地使用这一数据结构,优化代码性能。
215 13
|
6月前
|
Go
【LeetCode 热题100】DP 实战进阶:最长递增子序列、乘积最大子数组、分割等和子集(力扣300 / 152/ 416 )(Go语言版)
本文深入解析三道经典的动态规划问题:**最长递增子序列(LIS)**、**乘积最大子数组** 和 **分割等和子集**。 - **300. LIS** 通过 `dp[i]` 表示以第 `i` 个元素结尾的最长递增子序列长度,支持 O(n²) 动态规划与 O(n log n) 的二分优化。 - **152. 乘积最大子数组** 利用正负数特性,同时维护最大值与最小值的状态转移方程。 - **416. 分割等和子集** 转化为 0-1 背包问题,通过布尔型 DP 实现子集和判断。 总结对比了三题的状态定义与解法技巧,并延伸至相关变种问题,助你掌握动态规划的核心思想与灵活应用!
286 1
|
7月前
|
Go 索引
Go语言数组的定义与操作 - 《Go语言实战指南》
本文介绍了 Go 语言中的数组(Array)相关知识,包括定义、初始化方式(默认、显式、指定索引及自动推导长度)、访问与修改、遍历方法(for 循环和 for range)、值类型特性(复制行为)、多维数组支持以及其与切片的区别。数组是定长且同类型的集合,适合性能敏感场景,但实际开发中更常用动态的切片(slice)。
239 11
|
8月前
|
存储 算法 数据可视化
【二叉树遍历入门:从中序遍历到层序与右视图】【LeetCode 热题100】94:二叉树的中序遍历、102:二叉树的层序遍历、199:二叉树的右视图(详细解析)(Go语言版)
本文详细解析了二叉树的三种经典遍历方式:中序遍历(94题)、层序遍历(102题)和右视图(199题)。通过递归与迭代实现中序遍历,深入理解深度优先搜索(DFS);借助队列完成层序遍历和右视图,掌握广度优先搜索(BFS)。文章对比DFS与BFS的思维方式,总结不同遍历的应用场景,为后续构造树结构奠定基础。
412 10