刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • go语言基础

  • go语言进阶

  • go语言实现原理

    • channel 实现原理
    • string 实现原理
    • iota
    • slice 实现原理
      • slice 数据结构
      • slice 创建
        • 1. 使用make创建slice
        • 2. 使用array创建slice
      • slice 扩容
      • slice copy
      • slice 中的坑
        • 1. 数组坑
        • 2. 扩容坑
        • 3. 内存坑
      • 总结
    • map 实现原理
    • sync.Map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁 CAS 实现原理
    • defer实现原理
    • singleflight实现原理
    • timerate令牌桶限流 实现原理
  • gin框架

  • gorm

  • go测试

  • Go语言
  • go语言实现原理
bigox
2022-06-16
目录

slice 实现原理

  • slice又称动态数组,依托数组实现,可以方便的进行扩容、传递等
  • slice依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的Slice。
  • go语言切片是对数组的抽象。也可以被看做为"动态数组",数组的长度不可改变,切片长度可变
  • 在做函数调用时,slice 按引用传递,array 按值传递;

# slice 数据结构

  • 切片的结构包括三个部分:

    • 地址: 切片的地址一般指切片中的第一个元素所指向的内存地址, 用十六进制表示;
    • 长度: 切片实际存在元素的个数;
    • 容量: 从切片的起始元素开始到其底层数组中最后一个元素的个数;
    type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
    }
    
    1
    2
    3
    4
    5

# slice 创建

# 1. 使用make创建slice

  • 使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。

  • slice := make([]int, 5, 10)所创建的Slice,结构如下图所示:

    image-20220614190547744

  • 该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可

# 2. 使用array创建slice

  • 使用数组来创建Slice时,Slice将与原数组共用一部分内存。

    image-20220614190617190

  • 切片从数组array[5]开始,到数组array[7]结束(不含array[7]),即切片长度为2,数组后面的内容都作为切片的预留内存,即capacity为5

# slice 扩容

  • 使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

  • 扩容容量的选择遵循以下规则:

    • 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
    • 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
  • 使用append()向Slice添加一个元素的实现步骤如下:

    • 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice
    • 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice
      • 将新元素追加进新Slice,Slice.len++,返回新的Slice。

# slice copy

  • 使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。

  • copy过程中不会发生扩容。

# slice 中的坑

# 1. 数组坑

  • code

    
    func foo(a [2]int) {
    a[0] = 200
    }
    
    func main() {
    a := [2]int{1, 2}
    foo(a)
    fmt.Println(a)  // {1,2}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 在 Go 语言中,数组是一种值类型,而且不同长度的数组属于不同的类型。例如 [2]int 和 [20]int 属于不同的类型。

  • 当值类型作为参数传递时,参数是该值的一个拷贝,因此更改拷贝的值并不会影响原值。

  • 为了避免数组的拷贝,提高性能,建议传递数组的指针作为参数,或者使用切片代替数组。

# 2. 扩容坑

  • code

    func foo(a []int) {
    	a = append(a, 1, 2, 3, 4, 5, 6, 7, 8)
    	a[0] = 200
    }
    
    func main() {
    	a := []int{1, 2}
    	foo(a)
      fmt.Println(a)  // {1,2}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 切片是引用类型, 函数传参是引用传参, 但是foo函数中对a 进行append, 会对a扩容, a扩容后产生了新的切片a, 然后对新的切片a修改元素是不会对main函数中的a产生影响

  • 如果要对原来的切片进行更改

    func foo(a *[]int) {
    	*a = append(*a, 1, 2, 3, 4, 5, 6, 7, 8)
    	(*a)[0] = 200
    }
    
    func main() {
    	a := []int{1, 2}
    	foo(&a)
    	fmt.Println(a)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

# 3. 内存坑

  • 在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组。因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。比较推荐的做法,使用 copy 替代 re-slice。

    func lastNumsBySlice(origin []int) []int {
    	return origin[len(origin)-2:]
    }
    
    func lastNumsByCopy(origin []int) []int {
    	result := make([]int, 2)
    	copy(result, origin[len(origin)-2:])
    	return result
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 总结

  • 在做函数调用时,slice 按引用传递,array 按值传递;

  • 每个切片都保存了当前切片的长度、底层数组可用容量;

  • 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片;

  • 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片;

  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已;

  • 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片;

  • 创建切片时可根据实际需要预分配容量,尽量避免追加过程中扩容操作;

  • 切片拷贝时需要判断实际拷贝的元素个数;

  • 谨慎使用多个切片操作同一个数组,以防读写冲突;

#Go#
上次更新: 2023/04/16, 18:35:33
iota
map 实现原理

← iota map 实现原理→

最近更新
01
go与http代理
05-24
02
自制申威架构k8s-reloader
12-06
03
Docker Buildx 教程
12-01
更多文章>
Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式