golang特辑-简单记录一下slice底层实现

切片特性

golang中,存储数据的集合有两种类型,分别是数组(array)和切片(slice)。

其中切片是可变的长度,当切片长度不够时,golang会对其进行扩容
切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。


切片使用

仅记录一些常用操作

  • 创建切片:
	// 创建切片
	slice := make([]int, 1, 3) // 使用make可以创建切片的引用,第二个参数代表切片长度,第三个代表切片的容量,使用该种方法生成具有空值
	// 创建切片
	slice := make([]int, 1, 3) // 直接对切片进行初始化操作
  • 切片遍历:
	// 使用foreach遍历
	slice := []int{1, 2, 3}
	for _, val := range slice {
		fmt.Println(val)
	}
	// 直接使用索引进行遍历
	slice := []int{1, 2, 3}
	for i := 0; i < len(slice); i++ {
		fmt.Println(slice[i])
	}
  • 使用append追加切片元素:
	slice := make([]int, 3)
	for i := 0; i < 3; i++ {
		slice = append(slice, i)	// 该方式会在切片的末尾追加元素
	}
  • 切片拷贝:
	slice1 := []int{1, 2, 3}
	slice2 := make([]int, 3)
	copy(slice2, slice1)
  • 切片索引取值:
	slice := []int{1, 2, 3}
	fmt.Println(slice[0:2])	// 输出内容:[1 2], slice[0:2]代表去slice第[0]个元素,一直取到第[2]元素(不包括[2])
	fmt.Println(slice[0:1:2]) // 输出内容:[1], slice[0:1:2]第三个数字表示新切片容量为(2-0=2)

切片的数据结构

刚刚在切片特性内介绍到了golang中切片其实是值拷贝。
这里其实有一个疑问,如果切片元素内过多,那么每次传值不会特别特别慢吗?毕竟每次都要拷贝一个数组。别急,接下来介绍一下切片的数据结构。

数据结构

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

Slice 的数据结构定义如下:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

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

在这里插入图片描述
图片来自互联网


切片操作底层原理

  • 创建切片

    创建切片有两种形式,make 创建切片,空切片。

    func makeslice(et *_type, len, cap int) slice {
    	// 根据切片的数据类型,获取切片的最大容量
    	maxElements := maxSliceCap(et.size)
    	// 比较切片的长度,长度值域应该在[0,maxElements]之间
    	if len < 0 || uintptr(len) > maxElements {
      		panic(errorString("makeslice: len out of range"))
    	}
    	// 比较切片的容量,容量值域应该在[len,maxElements]之间
    	if cap < len || uintptr(cap) > maxElements {
        	panic(errorString("makeslice: cap out of range"))
    	}
    	// 根据切片的容量申请内存
    	p := mallocgc(et.size*uintptr(cap), et, true)
    	// 返回申请好内存的切片的首地址
    	return slice{p, len, cap}
    }
    
  • nil与空切片

    nil与空切片在项目中也是比较常用的,比如一些接口需要返回一个切片,但是程序出错时,就需要返回一个nil的切片或者带有空值的切片。

    在这里插入图片描述

    在这里插入图片描述

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

  • 拷贝切片

    这里直接上源码:

    func slicecopy(to, fm slice, width uintptr) int {
        // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 
        if fm.len == 0 || to.len == 0 {
            return 0
        }
        // n 记录下源切片或者目标切片较短的那一个的长度
        n := fm.len
        if to.len < n {
            n = to.len
        }
        // 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度
        if width == 0 {
            return n
        }
        // 如果开启了竞争检测
        if raceenabled {
            callerpc := getcallerpc(unsafe.Pointer(&to))
            pc := funcPC(slicecopy)
            racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
            racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
        }
        // 如果开启了 The memory sanitizer (msan)
        if msanenabled {
            msanwrite(to.array, uintptr(n*int(width)))
            msanread(fm.array, uintptr(n*int(width)))
        }
    
        size := uintptr(n) * width
        if size == 1 { 
            // TODO: is this still worth it with new memmove impl?
            // 如果只有一个元素,那么指针直接转换即可
            *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
        } else {
            // 如果不止一个元素,那么就把 size 个 bytes 从 fm.array 地址开始,拷贝到 to.array 地址之后
            memmove(to.array, fm.array, size)
        }
        return n
    }
    

    在这个方法中,slicecopy 方法会把源切片值(即 fm Slice )中的元素复制到目标切片(即 to Slice )中,并返回被复制的元素个数,copy 的两个类型必须一致。slicecopy 方法最终的复制结果取决于较短的那个切片,当较短的切片复制完成,整个复制过程就全部完成了。
    在这里插入图片描述

  • 切片扩容

    func growslice(et *_type, old slice, cap int) slice {
        if raceenabled {
            callerpc := getcallerpc(unsafe.Pointer(&et))
            racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
        }
        if msanenabled {
            msanread(old.array, uintptr(old.len*int(et.size)))
        }
    
        if et.size == 0 {
            // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
            if cap < old.cap {
                panic(errorString("growslice: cap out of range"))
            }
    
            // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
            return slice{unsafe.Pointer(&zerobase), old.len, cap}
        }
    
      // 这里就是扩容的策略
        newcap := old.cap
        doublecap := newcap + newcap
        if cap > doublecap {
            newcap = cap
        } else {
            if old.len < 1024 {
                newcap = doublecap
            } else {
                for newcap < cap {
                    newcap += newcap / 4
                }
            }
        }
    
        // 计算新的切片的容量,长度。
        var lenmem, newlenmem, capmem uintptr
        const ptrSize = unsafe.Sizeof((*byte)(nil))
        switch et.size {
        case 1:
            lenmem = uintptr(old.len)
            newlenmem = uintptr(cap)
            capmem = roundupsize(uintptr(newcap))
            newcap = int(capmem)
        case ptrSize:
            lenmem = uintptr(old.len) * ptrSize
            newlenmem = uintptr(cap) * ptrSize
            capmem = roundupsize(uintptr(newcap) * ptrSize)
            newcap = int(capmem / ptrSize)
        default:
            lenmem = uintptr(old.len) * et.size
            newlenmem = uintptr(cap) * et.size
            capmem = roundupsize(uintptr(newcap) * et.size)
            newcap = int(capmem / et.size)
        }
    
        // 判断非法的值,保证容量是在增加,并且容量不超过最大容量
        if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
            panic(errorString("growslice: cap out of range"))
        }
    
        var p unsafe.Pointer
        if et.kind&kindNoPointers != 0 {
            // 在老的切片后面继续扩充容量
            p = mallocgc(capmem, nil, false)
            // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
            memmove(p, old.array, lenmem)
            // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
            memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
        } else {
            // 重新申请新的数组给新切片
            // 重新申请 capmen 这个大的内存地址,并且初始化为0值
            p = mallocgc(capmem, et, true)
            if !writeBarrier.enabled {
                // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
                memmove(p, old.array, lenmem)
            } else {
                // 循环拷贝老的切片的值
                for i := uintptr(0); i < lenmem; i += et.size {
                    typedmemmove(et, add(p, i), add(old.array, i))
                }
            }
        }
        // 返回最终新切片,容量更新为最新扩容之后的容量
        return slice{p, old.len, newcap}
    }
    

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

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

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

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


更加详细的内容,可以参考:https://www.jianshu.com/p/030aba2bff41

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
个人学习golang笔记,从各种教程中总结而来,作为入门参考。目录如下 目录 1. 入门 1 1.1. Hello world 1 1.2. 命令行参数 2 2. 程序结构 3 2.1. 类型 4 2.1.1. 命名类型(named type)与未命名类型(unamed type) 4 2.1.2. 基础类型(underlying type) 4 2.1.3. 可赋值性 5 2.1.4. 类型方法集 6 2.1.5. 类型声明 6 2.2. 变量 8 2.2.1. 变量声明 8 2.2.2. 类型零值 12 2.2.3. 指针 13 2.3. 赋值 17 2.4. 包和文件 17 2.5. 作用域 18 2.6. 语句 19 2.7. 比较运算符 20 2.8. 类型转换 21 2.9. 控制流 23 2.9.1. If 23 2.9.2. Goto 24 2.9.3. For 25 2.9.4. Switch 25 2.9.5. break语句 31 2.9.6. Continue语句 31 3. 基础数据类型 31 3.1. golang类型 31 3.2. Numeric types 32 3.3. 字符串 33 3.3.1. 什么是字符串 33 3.3.2. 字符串底层概念 35 3.3.3. 获取每个字节 38 3.3.4. Rune 39 3.3.5. 字符串的 for range 循环 40 3.3.6. 用字节切片构造字符串 41 3.3.7. 用rune切片构造字符串 42 3.3.8. 字符串的长度 42 3.3.9. 字符串是不可变的 42 3.3.10. UTF8(go圣经) 43 3.4. 常量 45 3.4.1. 常量定义 45 3.4.2. 常量类型 46 3.4.3. Iota 46 4. 组合数据类型 47 4.1. 数组 47 4.1.1. 数组概述 47 4.1.2. 数组的声明 49 4.1.3. 数组的长度 50 4.1.4. 遍历数组 50 4.1.5. 多维数组 51 4.2. 切片 52 4.2.1. 什么是切片 52 4.2.2. 切片概述 55 4.2.3. 创建一个切片 55 4.2.4. 切片遍历 57 4.2.5. 切片的修改 58 4.2.6. 切片的长度和容量 60 4.2.7. 追加切片元素 62 4.2.8. 切片的函数传递 65 4.2.9. 多维切片 66 4.2.10. 内存优化 67 4.2.11. nil slice和empty slice 69 4.2.12. For range 70 4.3. 结构 71 4.3.1. 什么是结构体? 71 4.3.2. 结构体声明 73 4.3.3. 结构体初始化 77 4.3.4. 嵌套结构体(Nested Structs) 81 4.3.5. 匿名字段 82 4.3.6. 导出结构体和字段 84 4.3.7. 结构体相等性(Structs Equality) 85 4.4. 指针类型 86 4.5. 函数 87 4.6. map 87 4.6.1. 什么是map 87 4.6.2. 声明、初始化和make 89 4.6.3. 给 map 添加元素 91 4.6.4. 获取 map 中的元素 91 4.6.5. 删除 map 中的元素 92 4.6.6. 获取 map 的长度 92 4.6.7. Map 的相等性 92 4.6.8. map的排序 92 4.7. 接口 93 4.7.1. 什么是接口? 93 4.7.2. 接口的声明与实现 96 4.7.3. 接口的实际用途 97 4.7.4. 接口的内部表示 99 4.7.5. 空接口 102 4.7.6. 类型断言 105 4.7.7. 类型选择(Type Switch) 109 4.7.8. 实现接口:指针接受者与值接受者 112 4.7.9. 实现多个接口 114 4.7.10. 接口的嵌套 116 4.7.11. 接口的零值 119 4.8. Channel 120 4.9. 类型转换 120 5. 函数 120 5.1. 函数的声明 121 5.2. 一个递归函数的例子( recursive functions) 121 5.3. 多返回值 121 5.4. 命名返回值 121 5.5. 可变函数参数 122 5.6. Defer 123 5.6.1. Defer语句介绍 123 5.6.2. Defer使用场景 128 5.7. 什么是头等(第一类)函数? 130 5.8. 匿名函数 130 5.9. 用户自定义的函数类型 132 5.10. 高阶函数(装饰器?) 133 5.10.1. 把函数作为参数,传递给其它函数 134 5.10.2. 在其它函数中返回函数 134 5.11. 闭包 135 5.12. 头等函数的实际用途 137 6. 微服务创建 140 6.1. 使用net/http创建简单的web server 140 6.2. 读写JSON 144 6.2.1. Marshal go结构到JSON 144 6.2.2. Unmarshalling JSON 到Go结构 146 7. 方法 146 7.1. 什么是方法? 146 7.2. 方法示例 146 7.3. 函数和方法区别 148 7.4. 指针接收器与值接收器 153 7.5. 那么什么时候使用指针接收器,什么时候使用值接收器? 155 7.6. 匿名字段的方法 156 7.7. 在方法中使用值接收器 与 在函数中使用值参数 157 7.8. 在方法中使用指针接收器 与 在函数中使用指针参数 159 7.9. 在非结构体上的方法 161 8. 并发入门 162 8.1. 并发是什么? 162 8.2. 并行是什么? 162 8.3. 从技术上看并发和并行 163 8.4. Go 对并发的支持 164 9. Go 协程 164 9.1. Go 协程是什么? 164 9.2. Go 协程相比于线程的优势 164 9.3. 如何启动一个 Go 协程? 165 9.4. 启动多个 Go 协程 167 10. 信道channel 169 10.1. 什么是信道? 169 10.2. 信道的声明 169 10.3. 通过信道进行发送和接收 169 10.4. 发送与接收默认是阻塞的 170 10.5. 信道的代码示例 170 10.6. 信道的另一个示例 173 10.7. 死锁 174 10.8. 单向信道 175 10.9. 关闭信道和使用 for range 遍历信道 176 11. 缓冲信道和工作池(Buffered Channels and Worker Pools) 179 11.1. 什么是缓冲信道? 179 11.2. 死锁 182 11.3. 长度 vs 容量 183 11.4. WaitGroup 184 11.5. 工作池的实现 186 12. Select 188 12.1. 什么是 select? 188 12.2. 示例 189 12.3. select 的应用 190 12.4. 默认情况 190 12.5. 死锁与默认情况 191 12.6. 随机选取 191 12.7. 这下我懂了:空 select 191 13. 文件读写 191 13.1. GoLang几种读文件方式的比较 197 14. 个人 197 14.1. ++,-- 198 14.2. 逗号 198 14.3. 未使用的变量 199 14.4. Effective go 199 14.4.1. 指针 vs. 值 199 14.5. 可寻址性-map和slice的区别 201 14.5.1. slice 201 14.5.2. map 202 14.6. golang库 203 14.6.1. unicode/utf8包 203 14.6.2. time包 205 14.6.3. Strings包 205 14.6.4. 输入输出 212 14.6.5. 正则处理 224 14.6.6. Golang内建函数 226

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值