Slice和MAP底层实现

Slice切片的数据结构

切片本身并不是动态数组或者数组指针。它内部实现的数据结构是通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

切片是一个长度可变的数组。

Slice 的数据结构定义如下:

type slice struct {

    array unsafe.Pointer

    len   int  存储的数据长度

    cap   int  实际创建的slice的最大长度

}

切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。

如果想从 slice 中得到一块内存地址,可以这样做:

s := make([]byte, 200)

ptr := unsafe.Pointer(&s[0])

如果反过来呢?从 Go 的内存地址中构造一个 slice。

var ptr unsafe.Pointer

var s1 = struct {

    addr uintptr

    len int

    cap int

}{ptr, length, length}

s := *(*[]byte)(unsafe.Pointer(&s1))

Go 中切片扩容的策略是这样的:

如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。

一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。

注意:扩容扩大的容量都是针对原来的容量(cap )而言的,而不是针对原来数组的长度而言的。

是否创建新的地址

扩容以后并没有新建一个新的数组,扩容前后的数组都是同一个,这也就导致了新的切片修改了一个值,也影响到了老的切片了。并且 append() 操作也改变了原来数组里面的值。一个 append() 操作影响了这么多地方,如果原数组上有多个切片,那么这些切片都会被影响!无意间就产生了莫名的 bug!

这种情况,由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,所以这种情况下,扩容以后的数组还是指向原来的数组。

这种情况也极容易出现在字面量创建切片时候,第三个参数 cap 传值的时候,如果用字面量创建切片,cap 并不等于指向数组的总容量,那么这种情况就会发生。

    slice := array[1:2:3]

上面这种情况非常危险,极度容易产生 bug 。

建议用字面量创建切片的时候,cap 的值一定要保持清醒,避免共享原数组导致的 bug。

情况二:

情况二其实就是在扩容策略里面举的例子,在那个例子中之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。

所以建议尽量避免情况一,尽量使用情况二,避免 bug 产生。 

new

new是一个内置的函数,它的函数签名如下:

    func new(Type) *Type

其中,

    1.Type表示类型,new函数只接受一个参数,这个参数是一个类型

    2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:

func main() {

    a := new(int)

    b := new(bool)

    fmt.Printf("%T\n", a) // *int

    fmt.Printf("%T\n", b) // *bool

    fmt.Println(*a)       // 0

    fmt.Println(*b)       // false

}

本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:

func main() {

    var a *int

    a = new(int)

    *a = 10

    fmt.Println(*a)

}

make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:

func main() {

    var b map[string]int

    b = make(map[string]int, 10)

    b["测试"] = 100

    fmt.Println(b)

}

new与make的区别

    1.二者都是用来做内存分配的。

    2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;

    3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

Map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

结构上类似json

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

    make(map[KeyType]ValueType, [cap])

Map实现原理

最通俗的话说Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,我们通过key进行hash运算(可以简单理解为把key转化为一个整形数字)然后对数组的长度取余,得到key存储在数组的哪个下标位置,最后将key和value组装为一个结构体,放入数组下标处,看下图:

    length = len(array) = 4

    hashkey1 = hash(xiaoming) = 4

    index1  = hashkey1% length= 0

    hashkey2 = hash(xiaoli) = 6

    index2  = hashkey2% length= 2

函数参数

当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

没有指针属于  值传递   不影响参数原本的只

x=3

y=4

func swap(x, y int) int {

       ... ...

  }

x,y的值不变

 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数

/* 定义相互交换值的函数 */

func swap(x, y *int) {

    var temp int

    temp = *x /* 保存 x 的值 */

    *x = *y   /* 将 y 值赋给 x */

    *y = temp /* 将 temp 值赋给 y*/

}

func main() {

    var a, b int = 1, 2

    /*

        调用 swap() 函数

        &a 指向 a 指针,a 变量的地址

        &b 指向 b 指针,b 变量的地址

    */

    swap(&a, &b)

    fmt.Println(a, b)

}

输出结果:

    2 1

测试代码

package main

import (

    "fmt"

)

/* 定义相互交换值的函数    深拷贝*/

func swap(x, y *int) {

    var temp int

    temp = *x /* 保存 x 的值 */

    *x = *y   /* 将 y 值赋给 x */

    *y = temp /* 将 temp 值赋给 y*/

}

/* 定义相互交换值的函数  浅拷贝*/

func swapSmall(x, y int) {

    var temp int

    temp = x

    x = y

    y = temp

}

func main() {

    var a, b int = 1, 2

    /*

       调用 swap() 函数

       &a 指向 a 指针,a 变量的地址

       &b 指向 b 指针,b 变量的地址

    */

    swapSmall(a, b)

    fmt.Println(a, b)   // 1 2

    swap(&a, &b)

    fmt.Println(a, b)   // 2 1

}

输出结果

1 2

2 1

注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝引用传递是地址的拷贝,一般来说,地址拷贝更为高效而值拷贝取决于拷贝的对象大小,对象越大,则性能越低

注意2:map、slice、chan、指针、interface默认以引用的方式传递

多个参数传值

func myfunc(args ...int) {    //0个或多个参数

}

func add(a int, args…int) int {    //1个或多个参数

}

func add(a int, b int, args…int) int {    //2个或多个参数

}

 注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.

测试代码

package main

import (

    "fmt"

)

func test(s string, n ...int) string {

    var x int

    for _, i := range n {

        x += i

    }

    return fmt.Sprintf(s, x)

}

func main() {

    println(test("sum: %d", 1, 2, 3))

}

输出结果:

    sum: 6

使用 slice 对象做变参时,必须展开。(slice...)

package main

import (

    "fmt"

)

func test(s string, n ...int) string {

    var x int

    for _, i := range n {

        x += i

    }

    return fmt.Sprintf(s, x)

}

func main() {

    s := []int{1, 2, 3}

    res := test("sum: %d", s...)    // slice... 展开slice

    println(res)

}

面试题

面试题汇总-地鼠文档

8、slice,len,cap,共享,扩容

append函数,因为slice底层数据结构是,由数组、len、cap组成,所以,在使用append扩容时,会查看数组后面有没有连续内存快,有就在后面添加,没有就重新生成一个大的数组

在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存,将原 slice 拷贝到新 slice ,然后返回新 slice。扩容后再将数据追加进去。

扩容操作只对容量,扩容后的 slice 长度不变,容量变化规则如下:

  1. 若 slice 容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。
  1. 若 slice 容量够用,则将新元素追加进去,slice.len++,返回原 slice
  1. 若 slice 容量不够用,将 slice 先扩容,扩容得到新 slice,将新元素追加进新 slice,slice.len++,返回新 slice。

切片 slice 的扩容

当使用 append(slice,data) 时候,Golang 会检查底层的数组的长度是否已经不够,如果长度不够,Golang 则会新建一个数组,把原数组的数据拷贝过去,再将 slice 中的指向数组的指针指向新的数组。

  1. 其中新数组的长度一般是老数组的俩倍,当然,如果一直是俩倍增加,那也会极大的浪费内存. 所以在老数组长度大于 1024 时候,将每次按照不小于 25% 的涨幅扩容.

9、map如何顺序读取

map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值。

 6. go 中除了加 Mutex 锁以外还有哪些方式安全读写共享变量?

Go 中 Goroutine 可以通过 Channel 进行安全读写共享变量。

7. golang中new和make的区别?

用new还是make?到底该如何选择?

  1. make 仅用来分配及初始化类型为 slice、map、chan 的数据。
  1. new 可分配任意类型的数据,根据传入的类型申请一块内存,返回指向这块内存的指针,即类型 *Type。
  1. make 返回引用,即 Type,new 分配的空间被清零, make 分配空间后,会进行初始。

8. Go中对nil的Slice和空Slice的处理是一致的吗?

首先Go的JSON 标准库对 nil slice 和 空 slice 的处理是不一致。

  1. slice := make([]int,0):slice不为nil,但是slice没有值,slice的底层的空间是空的。
  1. slice := []int{} :slice的值是nil,可用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。

Map-New

​map 字典是 golang 中高级类型之一,它提供键值对形式的存储. 它也是引用类型,参数传递时其内部的指针被复制,指向的还是同一个内存地址. 当对赋值后的左值进行修改时,是会影响到原 map 值的.

map 的底层本质上是实现散列表它解决碰撞的方式是拉链法. map 在进行扩容时不会立即替换原内存,而是慢慢的通过 GC 方式释放.

hmap 结构

以下是 map 的底层结构,其源码位于 src/runtime/map.go 中,结构体主要是 hmap .

// A header for a Go map.

type hmap struct {

  // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.

  // Make sure this stays in sync with the compiler's definition.

  count     int // # live cells == size of map.  Must be first (used by len() builtin)

  flags     uint8

  B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)

  noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details

  hash0     uint32 // hash seed

  buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.

  oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing

  nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

  extra *mapextra // optional fields

}

上述代码中 buckets、oldbuckets 是指向存储键值的内存地址, 其中 oldbuckets 用于在扩容时候,指向旧的 bucket 地址,再下次访问时不断的将 oldbuckets 值转移到 buckets 中. oldbuckets 并不直接释放内存,而是通过不引用,交由 gc 释放内存.

散列表和 bucket ( a bucket for a go map)

hmap 中核心的结构是 buckets它是 bucket 数组,其中每个 bucket 是一个链表. 这个结构其实就是散列表的实现,通过拉链法消除 hash 冲突. 使得散列表能够存储更多的元素,同时避免过大的连续内存申请. 如下图 1,是 golang buckets 数组在内存中的形式,buckets 数组的每个元素是链表的头节点.

 在哈希表结构中有一个加载因子(即 loadFactor), 它一般是散列包含的元素数除以位置总数. 加载因子越高,冲突产生的概率越高. 当达到一定阈值时,就该为哈希表进行扩容了,否则查询效率将会很低.

当 golang map 的加载因子大于阈值时,len(map) / 2 ^ B > 6.5 时 ,就会对 map 对象进行扩容. 扩容不会立刻释放掉原来的 bucket 内存,而是由 oldbucket 指向,并产生新的 buckets 数组并由指针 buckets 指向. 在再次访问原数据时,再依次将老的 bucket 移到新的 buckets 数组中. 同时解除对老的 bucket 的引用,GC 会统一释放掉这些内存.

哈希函数

哈希函数是哈希表的特点之一,通过 key 值计算哈希,快速映射到数据的地址. golang 的 map 进行哈希计算后,将结果分为高位值和低位值,其中低位值用于定位 buckets 数组中的具体 bucket(先找链表在数组的位置),而高位值用于定位这个 bucket 链表中具体的 key(然后再找到该数据改链表的某个key)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值