2

声明

本系列文章并不会停留在Go语言的语法层面,更关注语言特性、学习和使用中出现的问题以及引起的一些思考。

要点

本文关注Go语言数组和切片相关的语言特性。

数组和切片以及字符串的关系

相同点

Go语言中数组、字符串和切片三者是密切相关的数据结构。这三种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。

差别

  1. Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。
  2. Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。
  3. Go语言切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参数时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。

给大家贴个数组切片关系图:

空切片和nil切片不是一个切片

var (
    a []int      // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
    b = []int{}  // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
)

其中:空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。

空切片会分配底层数组么?

在Go语言中我们通常会以[]int{}的方式去定义一个切片,这种切片我们称为空切片,我们知道,它是一个空的集合,那么他会分配相应的底层数组的内存空间么?我们来看一下下面这个例子:

func main() {
    var c = make( []int , 0 )
    pc := (*reflect.SliceHeader)(unsafe.Pointer(&c))
    fmt.Println(pc.Data) //824634224416
}

我们会发现,它是会指向一个地址的,那么它指向的地址会是底层数组的地址么?我们看一下源码是怎么样的?

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    if size == 0 {
        return unsafe.Pointer(&zerobase)
    }
    ...
    }
}

根据上边源码发现,在切片大小为0时,直接返回了一个内存地址,这点我们需要注意上边的那个结论:空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。

append对于数组和切片

首先,数组是不能使用append方法的,也就是说数组在声明的时候声明了长度之后是没办法扩容的,而slice可以,这也是为什么大家都普遍使用slice的原因。我们看下代码:

func main() {
    var a = []int{1,3,4,5}
    a = append(a, 5)
    fmt.Println(a) //[1 3 4 5 5]
    var b = [4]int{1,3,4,5}
    b = append(b, 5) //cannot use 'b' (type [4]int) as type []Type
}

既然扩容是存在的,那么他也有一定的规则:在添加元素时候,若原本slice的容量不够了,则会自动扩大一倍cap,在扩大cap时候是将原来元素复制一份(而不是引用),即这种情况下原有的不会变。

func main() {
    var a = []int{}
    //数组和切片len,cap,数组len和cap是定义的长度,切片是len是实际值,cap是数组容量
    fmt.Println(cap(a)) //0
    a = append(a, 5)
    fmt.Println(cap(a)) //1
    a = append(a, 5)
    fmt.Println(cap(a)) //2
    a = append(a, 5)
    fmt.Println(cap(a)) //4
    a = append(a, 5)
    fmt.Println(cap(a)) //4
     a = append(a, 5)
    fmt.Println(cap(a)) //8
}

append方法为什么不在内部修改以及实现方式

在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。即append函数总会返回新的切片,而且如果新切片的容量比原切片的容量更大那么就意味着底层数组也是新的了。试想一下,如果这里直接修改原切片,会发生什么呢?比如,A和B都是公用的底层数组Array,那么在append(A,1)的时候就可能会对B造成影响,但是如果返回指向新数组的切片,这种影响是不是就没有了呢?例子:

type stringStruct struct {
    array unsafe.Pointer  // 指向一个 [len]byte 的数组
    length int             // 长度
}

func main() {
    var a = []int{}
    p := (*stringStruct)(unsafe.Pointer(&a))
    fmt.Println(&p,p,cap(a)) //0xc000092018 &{0x1193a78 0} 0
    a = append(a, 5)
    p1 := (*stringStruct)(unsafe.Pointer(&a))
    fmt.Println(&p1,p1,cap(a)) //0xc000092028 &{0xc000098008 1} 1
    a = append(a, 5)
    p2 := (*stringStruct)(unsafe.Pointer(&a))
    fmt.Println(&p2,p2,cap(a))0xc000092030 &{0xc000098030 2} 2
    a = append(a, 5)
    p3 := (*stringStruct)(unsafe.Pointer(&a))
    fmt.Println(&p3,p3,cap(a))//0xc000092040 &{0xc00009a040 3} 4  
    a = append(a, 5)
    p4 := (*stringStruct)(unsafe.Pointer(&a))
    fmt.Println(&p4,p4,cap(a))//0xc000092040 &{0xc00009a040 4} 4 无需扩容,地址不变
    a = append(a, 5)
    p5 := (*stringStruct)(unsafe.Pointer(&a))
    fmt.Println(&p5,p5,cap(a))//0xc000092048 &{0xc000090080 5} 8
}

另外,切片赋值会导致底层数据的变化,从而影响其它的切片值,例如:

func main() {
    var c = [4]int{1,2,3,4}
    var Aslice = c[0:2]
    Aslice = append(Aslice,5)
    fmt.Println(c) //[1 2 5 4] 改变了底层数组
    fmt.Println(Aslice) //[1 2 5]
    Bslice := append(Aslice,5,5,5) //扩容超过底层数组的容量
    fmt.Println(c) //[1 2 5 4]
    fmt.Println(Bslice) //[1 2 5 5 5 5] //指向了新的数组
}    

切片是引用类型

func main() {
    //a是一个数组,注意数组是一个固定长度的,初始化时候必须要指定长度,不指定长度的话就是切片了
    a := [3]int{1, 2, 3}
    //b是数组,是a的一份拷贝
    b := a
    //c是切片,是引用类型,底层数组是a
    c := a[:]
    for i := 0; i < len(a); i++ {
    a[i] = a[i] + 1
    }
     //改变a的值后,b是a的拷贝,b不变,c是引用,c的值改变
    fmt.Println(a) //[2,3,4]
    fmt.Println(b) //[1 2 3]
    fmt.Println(c) //[2,3,4]
}

因为切片赋值和函数传参数时也是将切片头信息部分按传值方式处理,所以会出现引用失效的问题:

func main() {
    var a = []int{4, 2, 5, 7, 2, 1, 88, 1} 
    delete(a)
    fmt.Println(a) //1:[4 2 5 7 2 1 88 1]  2:[1 2 5 7 2 1 88 1]
      
}
func delete(a []int){
    a = a[:len(a)-1]//1:[4 2 5 7 2 1 88]
    a[0] = 1 //2:[1 2 5 7 2 1 88 1]
    fmt.Println(a) 
}

切片和数组
在函数传参中是值传递,所以会copy一份原始的切片,但是指向底层数组的指针不变,如果我们在函数中对这个copy过的切片操作(非赋值),例如重新进行切片操作,这样不会影响原切片,但是如果我们在此进行例如a[0]=1此类的操作,会修改原数组。

判等

对于数组来说,依次比较各个元素的值。根据元素类型的不同,再依据是基本类型、复合类型、引用类型或接口类型,按照特定类型的规则进行比较。所有元素全都相等,数组才是相等的。例如:

func main() {
    var a = [4]int{1,3,4,5}
    var b = [4]int{1,3,4,5}
    var c = [4]int{2,3,4,5}
    var d = [5]int{}
    var e = [6]int{}
    var f = [4]string{"2","3","4","5"}
           
    fmt.Println(a == b) //true
    fmt.Println(a == c) //false
    fmt.Println(d == e) //mismatched types [5]int and [6]int
    fmt.Println(f == c) //mismatched types [4]int and [4]string
}

对于slice来说来说,在Go语言当中,切片类型是不可比较的。并且所有含有切片的类型都是不可比较的,因为不可比较性会传递,如果一个结构体由于含有切片字段不可比较,那么将它作为元素的数组不可比较,将它作为字段类型的结构体不可比较。那么为什么不可比较呢?我们看一下官方给的回答:

Why don't maps allow slices as keys?
Map lookup requires an equality operator, which slices do not implement. They don't implement equality because equality is not well defined on such types; there are multiple considerations involving shallow vs. deep comparison, pointer vs. value comparison, how to deal with recursive types, and so on. We may revisit this issue—and implementing equality for slices will not invalidate any existing programs—but without a clear idea of what equality of slices should mean, it was simpler to leave it out for now.        

大致意思就是说切片在比较时需要考虑的因素太多太多,例如是比较切片的内容还是底层数组的内容等等,所以Go语言不允许slice比较,当然slice可以和nil比较。

切片,数组可进行赋值操作

基本规则:对于每个赋值一定要类型一致,和其他一样,不同的类型不可以进行赋值操作。当然,interface{}例外。

func main() {
    //切片
    var a = make([]string,10)
    //a[0] = 1 //赋值其他类型均报错
    a[0] = "grape"
                    
    //数组
    var a = [3]int{}
    a[0] =1
    a[1] = "strin" //赋值其他类型均报错
}   

切片,数组和字符串的循环

切片数组字符串循环代码示例:

func main() {
    var a = [3]int{1,2,3}
    for i,v := range(a) { 
        fmt.Println(i,v)  // 0 1   1 2  2 3
     }
        
    var b = []int{3,4,5}
    for ide,v := range(b) {
        fmt.Println(i,v)  //0 3  1 4  2 5
    }
        
    var c = "hello world"
    hello := c[:5]  
    world := c[7:]
    fmt.Println(hello, world)  //hello  world
    for i,v := range(c) {
        fmt.Println(i,string(v))  // 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd',
        fmt.Println(i,v)  //0 104 1 101 2 108 3 108 4 111 5 32 6 19990 9 30028 //range会转化底层byte为rune
    }
}

切片类型强转

对于一个float类型的切片转换为int型,我们要怎么转?是直接转换么?还是新开辟一个切片去做每一项强转?我们看下边的示例:

func main() {
    var a = []float64{4, 2, 5, 7, 2, 1, 88, 1}
    //var c = ([]int)(a) //报错
    var b = make([]int, 8)
    for i,v := range a {
        b[i] = int(v)
    }
    fmt.Println(b)
}

下期预告

【Go语言踩坑系列(四)】字典

关注我们

欢迎对本系列文章感兴趣的读者订阅我们的公众号,关注博主下次不迷路~

Nosay


NoSay
449 声望544 粉丝