Golang理解-数组和切片

数组

数组在Go中定义及特点

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。

因为数组的长度是固定的,因此在Go语言中很少直接使用数组。

和数组对应的类型是Slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活,但是要理解slice工作原理的话需要先理解数组。

默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组:

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。因此,上面q数组的定义可以简化为

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

我们将会发现,数组、slice、map和结构体字面值的写法都很相似。上面的形式是直接提供顺序初始化值序列,但是也可以指定一个索引和对应值列表的方式初始化,就像下面这样:

type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。

数组如何在函数参数中传递

当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。

因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量

在这个方面,Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。

注意事项

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,

但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。

而且数组也没有任何添加或删除数组元素的方法。由于这些原因,除了一些需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。


Slice

slice在go中的定义及特点

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。

一个slice由三个部分构成:指针、长度和容量。

  • 指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素
  • 长度对应slice中元素的数目
  • 长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

内置的len和cap函数分别返回slice的长度和容量。

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大

因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素

换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名

slice之间不能比较(这和数组不同),因此我们不能使用"=="操作符来判断两个slice是否含有全部相等元素。

不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较:

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }
    for i := range x {
        // 逐个元素比较
        if x[i] != y[i] {
            return false
        }
    }
    return true
}

为什么slice不支持比较呢?

  1. 一个slice的元素是间接引用的,一个slice甚至可以包含自身。虽然有很多办法处理这种情形,但是没有一个是简单有效的。
  2. 因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改. Go语言中map的key只做简单的浅拷贝,它要求key在整个生命周期内保持不变性(译注:例如slice扩容,就会导致其本身的值/地址变化)。
  3. 对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象

基于以上原因,我们安全的做法是直接禁止slice之间的比较操作,slice唯一合法的比较操作是和nil比较.

虽然slice是可以和nil进行比较的,但是只其中也有些细节需要注意:

  1. 一个零值的slice等于nil。一个nil值的slice并没有底层数组。
  2. 一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]
  3. 如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样;

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

slice扩容原则

先来看看slice源码中是如何写的,在来分析扩容的原则:

func growslice(et *_type, old slice, cap int) slice {
    if et.size == 0 {
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve old.array in this case.
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        // 小于1024,*2扩容
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // 大于1024,*1.25
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }
  // 下面代码省略
  ....
}

从上面代码可以看出, slice的扩容规则是:

  1. 小于1024,每次扩容后的cap = oldCap * 2
  2. 大于1024,每次扩容cap = oldCap * 1.25

我们可以看出,每次扩容会涉及到数组的copy,然后生成新的数组(slice指向新的数组),这样会给系统带来额外的开销,通常我们在创建slice的时候,建议使用make函数,更具业务场景给定一个合适的cap大小,避免slice因为扩容而发生底层数组的copy。

slice的内存使用技巧

案例一:

输入slice和输出slice共用一个底层数组,这可以避免分配另一个数组,不过原来的数据将可能会被覆盖:例如

func nonempty(strings []string) []string {
    i := 0
    for _, s := range strings {
        if s != "" {
            strings[i] = s
            i++
        }
    }
    return strings[:i]
}

// 输出:
data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data)           // `["one" "three" "three"]`

同样的,使用append也能实现同样的功能:

func nonempty2(strings []string) []string {
    out := strings[:0] // zero-length slice of original
    for _, s := range strings {
        if s != "" {
            out = append(out, s)
        }
    }
    return out
}

无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。

这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。

案例二:

使用slice来模拟stack操作,入栈即向slice中append元素,出栈则通过收缩slice,弹出栈顶的元素:

// 入栈, push
stack = append(stack, v)

// 出栈, pop
stack = stack[:len(stack)-1]

案例三:

要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:

list := []int{1,2,3,4,5,6}
// 删除元素i,并保留原来的顺序,原理就是将i后面的元素按次序copy
copy(list[i:],list[i+1:])

要删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:

func remove(slice []int, i int) []int {
    // 使用最后一个元素覆盖要删除的元素
    slice[i] = slice[len(slice)-1]
    // 返回新的slice
    return slice[:len(slice)-1]
}

原文地址:https://www.cnblogs.com/vinsent/p/11326417.html

时间: 2024-10-08 15:32:22

Golang理解-数组和切片的相关文章

GoLang笔记-数组和切片,本质是就是长度不可变的可变的区别

数组 Arrays 数组是内置(build-in)类型,是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值.在初始化后长度是固定的,无法修改其长度.当作为方法的入参传入时将复制一份数组而不是引用同一指针.数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度. 初始化 数组的初始化有多种形式,查看示例代码 , 在线运行示例代码 [5] int {1,2,3,4,5} 长度为5的数组,其元素值依次为:1,2,3,4,5 [5] int {1,2} 长度为5的数组

golang中数组与切片的区别

初始化:数组需要指定大小,不指定也会根据初始化的自动推算出大小,不可改变 数组: a := [...]int{1,2,3} a := [3]int{1,2,3} 切片: a:= []int{1,2,3} a := make([]int, 5) a := make([]int, 5, 10) 函数传递:数组需要明确指定大小,切片不需要.数组是值传递,切片是地址传递 numbers2 := [...]int{1, 2, 3, 4, 5, 6} maxIndex2 := len(numbers2) -

C++-Golang的数组类型异同

1. 本文章的必要性 C++的存在像一把瑞士军刀,继承了C语言的设计理念--"充分相信程序员",几乎将所有的底层细节都暴露在外,程序员可以自由控制.在最近详细学习Golang的过程中,发现Golang中的常用结构的设计理念与C++截然不同,为许多常用的操作都提供了语言设计者所认为的"最佳实践".本文章仅讨论Golang中数组和切片. 2. Golang数组 和 C++数组(Array) 例子 C++ version // const常量长度初始化 // 显然非常量无

Go 系列教程 —— 11. 数组和切片

数组 数组是同一类型元素的集合.例如,整数集合 5,8,9,79,76 形成一个数组.Go 语言中不允许混合不同类型的元素,例如包含字符串和整数的数组.(译者注:当然,如果是 interface{} 类型数组,可以包含任意类型) 数组的声明 一个数组的表示形式为 [n]T.n 表示数组中元素的数量,T 代表每个元素的类型.元素的数量 n 也是该类型的一部分(稍后我们将详细讨论这一点). 可以使用不同的方式来声明数组,让我们一个一个的来看. package main import ( "fmt&q

学习Golang语言(6):类型--切片

学习Golang语言(1): Hello World 学习Golang语言(2): 变量 学习Golang语言(3):类型--布尔型和数值类型 学习Golang语言(4):类型--字符串 学习Golang语言(5):类型--数组 学习Golang语言(6):类型--切片 在很多应用场景中,数组不能够满足我们的需求.在初始定义数组时,我们并不知道数组所需的长度.因此,我们需要一个大小可以动态变化的数组(动态数组) 在Go语言中,这种"动态数组"成为slice(切片). 但是实际上slic

Go数组、切片、映射的原理--简明解析

数组.切片.映射是Golang的最重要的数据结构,下面是对这3种数据结构的一点个人总结: 一.数组 数组是切片和映射的基础数据结构. 数组是一个长度固定的数据类型,存储着一段具有相同数据类型元素的连续内存块. 因为数组占用的内存是连续分配的,所以对数组的操作速度很快. 声明数组的方式:4种 var array1 [5]int array1 := [5]int{3,5,6,3,2} array1 := [...]int{3,4,7,8,1} //根据数组字面量中元素的个数来确定数组的长度 arra

Go语言入门——数组、切片和映射

按照以往开一些专题的风格,第一篇一般都是“从HelloWorld开始” 但是对于Go,思来想去,感觉真的从“HelloWorld”说起,压根撑不住一篇的篇幅,因为Go的HelloWorld太简单了. 1.简介 Go是什么? Go(又称Golang)是Google开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言.——摘自百度百科 Github地址 https://github.com/golang/go 官网地址 https://golang.org 中文网社区 https://

go 的数组和切片

什么是数组? 数组 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成 数组定义的方法? 方式一 package main import "fmt" func arraytest() { var x [3] int fmt.Println(x) } // 输出 [0 0 0] func main() { arraytest() } 使用快速声明数组 x3 :=[3] int {112,78} fmt.Println(x3) 输出 [112 78 0] //

Python中numpy 数组的切片操作

Python中numpy 数组的切片操作简介取元素 X[n0,n1]切片 X[s0:e0,s1:e1]切片特殊情况 X[:e0,s1:]示例代码输出结果简介X[n0,n1]是通过 numpy 库引用二维数组或矩阵中的某一段数据集的一种写法.类似的,X[n0,n1,n2]表示取三维数组,取N维数组则有N个参数,N-1个逗号分隔.以二维数组为例: import numpy as npX = np.array([[0,1,2,3],[10,11,12,13],[20,21,22,23],[30,31,