玩好go的切片

2023-10-31

go的slice ,入门就会遇到,但这个东西大多数人都是停留在简单的使用。一些干了好几年的老程序员都说不明白里面的道道,这里面坑不少。恰巧今天有空,好好整理下,永不踩坑

1、为什么要用切片

其他语言大多用的都是数组,在go中,数组的长度是不可变化的,声明后就是固定的,所以go有了切片,长度是可变化的我们平时用的最多的也是切片。

2、什么是切片

2.1基本概念

  1. slice
  2. 切片是数组的引用类型,故是引用传递(其实是值传递,下面会细细分析)
  3. 切片的使用和数组类似
  4. 切片的长度是可以变化的(动态变化的数组)
  5. 切片的定义语法:
    var 切片名 []数据类型(没有长度
    如 var laozhao []int

3、切片在内存中得布局

在这里插入图片描述

  1. 切片是数组的引用,所以出现顺序数组肯定是先于切片的(可以理解为切片是数组上的一个滑动窗口)
  2. 声明一个数组 intArr,首先在内存中开辟一个数组空间,空间中存放我们声明的数(这里值得是数组中22的地址)
  3. 然后我们声明了一个切片,切片是数组下标从第1开始到3之前的部分(包头不包尾)。此时在内存中会开辟另一块空间,切片的本质是一个结构体,一共三部分:切片中首元素在数组中得地址、长度、容量
  4. 改变切片中得数据会改变数组的值

切片数据结构本质是一个结构体
type slice struct{
ptr unsafe.Pointer
len int
cap int
}

4、值传递和引用传递

在了解切片的传递模式之前先了解下什么是值传递和引用传递

值传递:方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。

引用传递:也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。
在这里插入图片描述

5、切片是值传递还是引用传递

首先看这段代码的运行结果

func main() {
	var a = [3]int{6, 6, 6}
	ages := a[:]
	//fmt.Printf("%T\n", ages)
	//fmt.Printf("%p\n",&ages)
	//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
	//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
	fmt.Printf("原始slice的内存地址是%p\n", ages)
	modify(ages)
	fmt.Println(ages)
}

func modify(ages []int) {
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	ages[0] = 1
}

运行结果
在这里插入图片描述
这里看到的,初始切片的地址是 0xc0000a0120,传入 modify 函数后的地址还是 0xc0000a0120,并且在 modify 中对 切片的操作是可以影响到初始的切片的,

所以很多人就会说 切片是引用传递,其实不然,我们看下面代码

func main() {
	var a = [3]int{6, 6, 6}
	ages := a[:]
	//fmt.Printf("%T\n", ages)
	//fmt.Printf("%p\n",&ages)
	fmt.Printf("数组第一个元素的地址%v\n", &a[0])
	fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
	fmt.Printf("原始slice的内存地址是%p\n", ages)
	modify(ages)
	fmt.Println(ages)
}

func modify(ages []int) {
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	ages[0] = 1
}

运行结果
在这里插入图片描述
数组首地址、切片首地址、切片地址 都是一样的
恍然大悟,原来 %p 打印出来的时 切片中存入的数组的首地址,在函数传参时也传的是 地址的一个拷贝
为什么%p打印的是切片中指向数组的地址,是因为 fmt 内部对切片有特殊的处理

那么切片自身也是有地址的,就是下图红框圈出来的部分
在这里插入图片描述
看下切片自身的地址是什么

func main() {
	var a = [3]int{6, 6, 6}
	ages := a[:]
	//fmt.Printf("%T\n", ages)
	fmt.Printf("切片自身的地址 %p\n",&ages)
	//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
	//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
	fmt.Printf("原始slice的内存地址是%p\n", ages)
	modify(ages)
	fmt.Println(ages)
}

func modify(ages []int) {
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	fmt.Printf("切片自身的地址 %p\n",&ages)
	ages[0] = 1
}

运行结果
在这里插入图片描述
可以看到切片自身的地址在作为参数传入后是发生变化的,是值传递。之所以在函数中的操作能改变原切片中得值,是因为切片中存的数组首地址是相同的

当然,作为参数传入后,是无法修改原切片的len和cap的,如果len和cap发生了变化,指向的数组将发生变化,看下面这个代码

func main() {
	var a = [3]int{6, 6, 6}
	ages := a[:]
	//fmt.Printf("%T\n", ages)
	//fmt.Printf("切片自身的地址 %p\n", &ages)
	//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
	//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
	fmt.Printf("原始slice的内存地址是%p\n", ages)
	modify(ages)
	fmt.Println(ages)
}

func modify(ages []int) {
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	//fmt.Printf("切片自身的地址 %p\n", &ages)
	fmt.Println(len(ages),"   ", cap(ages))
	ages = append(ages, 6)
	fmt.Println(len(ages),"   ", cap(ages))
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	//fmt.Printf("切片自身的地址 %p\n", &ages)
}

运行结果
在这里插入图片描述
可以看到切片中得数组首地址发生了变化,原来的切片数据也没发生变化。

如果想修改原切片的的cap(如append操作),则需要传入指针,看下面演示

func main() {
	var a = [3]int{6, 6, 6}
	ages := a[:]
	//fmt.Printf("%T\n", ages)
	//fmt.Printf("切片自身的地址 %p\n", &ages)
	//fmt.Printf("数组第一个元素的地址%v\n", &a[0])
	//fmt.Printf("切片第一个元素的地址%v\n", &ages[0])
	fmt.Printf("原始slice的内存地址是%p\n", ages)
	modify(&ages) // 传入的是指针
	fmt.Println(ages)
}

func modify(ages *[]int) {
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	//fmt.Printf("切片自身的地址 %p\n", &ages)
	fmt.Println(len(*ages),"   ", cap(*ages))
	*ages = append(*ages, 6)
	fmt.Println(len(*ages),"   ", cap(*ages))
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	//fmt.Printf("切片自身的地址 %p\n", &ages)
}

可以看到通过传入切片指针,可以改变原切片的cap。原切片指向了新的数组
这里是引用

6、切片 appent 操作 相关的坑

append的时候要关注cap是否更改,如果cap更新的话,底层会指向新的数组(扩容后新建的数组)

func test(){
	var array =[]int{1,2,3,4,5}// len:5,capacity:5
	var newArray=array[1:3]// len:2,capacity:4   (已经使用了两个位置,所以还空两位置可以append)
	fmt.Printf("%p\n",array) //0xc420098000
	fmt.Printf("%p\n",newArray) //0xc420098008 可以看到newArray的地址指向的是array[1]的地址,即他们底层使用的还是一个数组
	fmt.Printf("%v\n",array) //[1 2 3 4 5]
	fmt.Printf("%v\n",newArray) //[2 3]

	newArray[1]=9 //更改后array、newArray都改变了
	fmt.Printf("%v\n",array) // [1 2 9 4 5]
	fmt.Printf("%v\n",newArray) // [2 9]

	newArray=append(newArray,11,12)//append 操作之后,array的len和capacity不变,newArray的len变为4,capacity:4。因为这是对newArray的操作
	fmt.Printf("%v\n",array) //[1 2 9 11 12] //注意对newArray做append操作之后,array[3],array[4]的值也发生了改变
	fmt.Printf("%v\n",newArray) //[2 9 11 12]

	newArray=append(newArray,13,14) // 因为newArray的len已经等于capacity,所以再次append就会超过capacity值,
	// 此时,append函数内部会创建一个新的底层数组(是一个扩容过的数组),并将array指向的底层数组拷贝过去,然后在追加新的值。
	fmt.Printf("%p\n",array) //0xc420098000
	fmt.Printf("%p\n",newArray) //0xc4200a0000
	fmt.Printf("%v\n",array) //[1 2 9 11 12]
	fmt.Printf("%v\n",newArray) //[2 9 11 12 13 14]  他两已经不再是指向同一个底层数组y了
}

7、切片的复制

切片复制可以用copy也可以用等号
等号是引用复制,复制的时切片内指向数组的指针及相关信息。底层指向的数组还是同一个
copy是完全进行的值拷贝,会在内存中开辟新的空间,建立新的数组。底层指向的数组发生变化

func main() {
	sl_from := []int{1, 2, 3}
	b := make([]int, len(sl_from))
	copy(b, sl_from)
	a := sl_from
	fmt.Printf("原切片:%p  copy后的:%p 等于后的:%p\n", sl_from, b, a)
	fmt.Printf("原切片:%p copy后的:%p 等于后的:%p\n", &sl_from, &b, &a)
	
}

在这里插入图片描述
根据结果可以看出,copy过的两个切片的地址是完全不同的
但是等于后的切片,指向的数组还是同一个

8、切片的扩容原理

切片底层的数据结构是一个结构体,里面保存了对底层数组的引用,长度 len,和 容量 cap
不指定 cap的情况下,len 和 cap 是相等的。
后期发生了append 操作的话,当cap 不足时,会进行扩容。cap和len是可能不相等的

8.1 例子

package main

import (
	"fmt"
)

func main() {
	a := []int{1, 2, 3, 4}
	fmt.Println(cap(a))
	for i := 0; i < 20; i++ {
		a = append(a, a...) // 进行双倍扩容
		if len(a) > 1024 {  // 当长度大于 1024 的时候我们就退出循环
			break
		}
		fmt.Print(cap(a), " ") // 最后一次扩容
	}
	fmt.Println(cap(a), len(a))
}

-
先看结果

  • 初始 切片的容量是 4
  • 在长度 1024 范围内时,扩容时 均是 进行 二倍扩容
  • 在长度超出 1024 范围时,会 进行 25% 扩容,知道 满足容量要求

8.2 源码分析

// src/runtime/slice.go
// go version 1.13
func growslice(et *_type, old slice, cap int) slice {
// ...省略部分
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        //原切片长度低于1024时直接翻倍
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            //原切片长度大于等于1024时,每次只增加25%,直到满足需要的容量
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
// ...省略部分
}
  • 可以看到 1024 之前 ,扩容都是 newcap = doublecap
  • 大于 1024 时 扩容就变成了循环扩容for 0 < newcap && newcap < cap { newcap += newcap / 4 }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

玩好go的切片 的相关文章

  • 深入了解golang 的channel

    文章目录 1 channel 是什么 channel的特点 2 channel 的数据结构 hchan 等待队列和发送队列的类型包装 sudog 3 channel 分类 有缓冲channel 无缓冲channel 4 channel 的创
  • 学习笔记(01):go快速入门-iota用法

    立即学习 https edu csdn net course play 26897 344142 utm source blogtoedu
  • go语言使用gin框架

    gin框架基础用法 package main import github com gin gonic gin net http func main router gin Default router LoadHTMLGlob templat
  • Go Web编程实战(7)----并发goroutine

    目录 什么是goroutine 使用方式 什么是goroutine 在Go语言中 每一个并发执行的活动被称为goroutine 使用go关键字可以创建goroutine 其完整定义如下 go func name 其中 go是关键字 需要放在
  • Go语言学习5-切片类型

    切片类型 引言 1 切片 1 1 类型表示法 1 2 值表示法 1 3 属性和基本操作 1 4 切片使用的复杂用法 总结 引言 上篇我们介绍了 Go语言的数组类型 本篇将介绍Go语言的切片类型 主要如下 1 切片 切片可以看作是对数组的一种
  • 微信小程序总结(2)- 需求分析

    在真正进入代码开发之前 很重要的一步就是进行需求分析 用户画像 这款微信小程序的主要用户是谁 是年轻人 中年人 还是老年人 是男生 还是女生 是工薪阶层 还是企业主 是金融理财 还是在线票务 在进行一定范围的样本调查后 可以得出一个精准的用
  • 使用 Go API 快速下载 excel 文件

    我们有几个 Golang API 可以为 csvfiles 提供服务 但在提供以编程方式生成的 excel 文件方面没有任何帮助 为了避免重新编写 我们可以借助此服务器开始 main go 这使我们能够服务于路由 和 excel downl
  • 【笔记】Go语言学习笔记

    一 概述 什么是程序 程序 为了让计算机执行某些操作或解决某个问题而编写的一系列有序指令的集合 Go语言 是区块链最主流的编程语言 同时也是当前最具发展潜力的语言 Go语言是Google公司创造的语言 也是Google主推的语言 Googl
  • Go 语言运行时环境变量快速导览

    原文 http dave cheney net 2015 11 29 a whirlwind tour of gos runtime environment variables Go 语言运行时环境变量快速导览 介绍 Go Runtime除
  • Go语言中的rune数据类型

    写在前面 最近开始学习Go语言 因为自己是从Java逐步转Go原因 在感慨Go语言简便的同时 也因为其封装的数据类型和包较多 所以还得慢慢学习 今天来谈谈Go语言中的rune数据类型 名词解释 Go语言中的整数类型也有有符号数和无符号数之别
  • go语言学习笔记1--flag代码包

    flag代码包用于接收和解析命令参数 我们以hello world代码作为示例 package main import fmt func main fmt Println hello world 现在 我们想要根据输入定制hello的对象
  • 42-Golang中的单元测试

    Golang中的单元测试 需求 传统方法 基本介绍 单元测试快速入门总结 综合案例 需求 在工作中 我们会遇到这样的情况 就是去确认一个函数 或者一个模块的结果是否正确 传统方法 在main函数中 调用addUpper函数 看看实际输出的记
  • Go-OpenWrt获取wan口ip、dns、网关ip

    Go OpenWrt获取wan口ip dns 网关ip 文章目录 Go OpenWrt获取wan口ip dns 网关ip 1 前言 2 解决方案思路 3 代码 1 前言 一般来说 Openwrt可以配置多个wan口和多个lan口 这里获取的
  • 【Go语言教程】(一) 下载、安装、配置

    1 下载 官网安装包下载地址为 https golang org dl 如果打不开可以使用这个地址 https golang google cn dl 找到适合你系统的版本下载 我下载的是windows版本 2 安装 msi文件点击完成安装
  • golang 详解协程——errgroup

    为什么要有sync errgroup go支持并发 一般采用的是 channel sync WaitGroup context 来实现各个协程之间的流程控制和消息传递 但是对于开启的成千上万的协程 如果在每个协程内都自行去打印 错误日志的话
  • 学习笔记(02):go快速入门-iota用法

    立即学习 https edu csdn net course play 26897 344142 utm source blogtoedu
  • 【笔记】Go语言 Http-client 解析json后并插入数据库

    一 Http client 获取json 参考 Go标准库http Client的连接行为控制详解 原创手记 慕课网 因为 代码 resp err http Get http example com resp err http Post h
  • Go语言的跨文件调包

    一 前言 文件的结构如下 go mod main main go util util go 文件在调用其它包的时候 需要在代码中引用其他的函数 包的命名一般都以小写为主 文件中函数的以首字符为大写的函数为共有函数 小写的为私有函数 只能是在
  • Go timer 是如何被调度的?

    hi 大家好 我是 haohongfan 本篇文章剖析下 Go 定时器的相关内容 定时器不管是业务开发 还是基础架构开发 都是绕不过去的存在 由此可见定时器的重要程度 我们不管用 NewTimer timer After 还是 timer
  • go语言学习 1 -- 类型

    Go语言接受了函数式编程的一些想法 支持匿名函数与闭包 接受了以Erlang语言为代表的面向消息编程思想 支持goroutine和通道 并推荐使用消息而不是共享内存来进行并发编程 总体来说 Go语言是一个非常现代化的语言 精小但非常强大 学

随机推荐

  • 从零开始学习React——(七):React列表循环数据以及事件绑定

    本节主要介绍React中列表循环展示数据以及事件的绑定 1 列表循环数据化 目前Child js组件中的 li 标签内的数据是静态的 也就是死的 如果要换成动态的 就需要把这个列表进行数据化之后再用JavaScript代码循环在页面上 首先
  • [systemc][tlm2.0]父模块与子模块的实现

    一 windows下环境配置 尝试1 visual studio 配置systemc环境 systemC学习笔记3 vs开发环境搭建 知乎 zhihu com 32 封私信 80 条消息 流浪码农 知乎 zhihu com 之前配置总是不通
  • pb_ds实现可重复set

    简单来说 就是将数据类型改为pair 保证值不同就行 less
  • 基于vue的picker组件

    概述 基于vue js选择器组件 github https github com xiecg vue DEMO vue picker 安装 npm install vue 3d picker save import picker from
  • 解决freemarker数组中的对象属性获取不到

    1 问题现象 使用Freemarker写入模板的时候 遍历List的时候发现对象中的首字母大写和带下划线的时候就会报错 The following has evaluated to null or missing FTL stack tra
  • 基于Rockchip RK3588 Android12 SDK搭建自己的repo 仓库服务器

    基于Rockchip RK3588 Android12 SDK搭建自己的repo 仓库服务器 文章目录 基于Rockchip RK3588 Android12 SDK搭建自己的repo 仓库服务器 搭建自己的repo代码服务器 流程框图 环
  • Markdown自定义CSS样式

    前言 当我第一次接触到Markdown时 我就深深爱上了它 这简洁的界面 编程式的书写都令我爱不释手 最重要的是 还能够支持自定义html css 自定义CSS样式 说到Markdown 就不得不提及Typora这个软件 本例子即是在此软件
  • 解决vue3类型“{}”上不存在属性

    刚创建的一个Vue3和Ts的项目 结果使用Vscode打开后 修改了index vue文件就报错了 修改tsconfig json文件 在tsconfig json文件中添加一行代码 就是让ts识别vue文件 include src ts
  • Ubuntu虚拟机中网络中没有网卡

    由于断电等异常操作 导致vmware的ubuntu系统连接不到网络 ping www baidu com 提示 name or service not known 查看网卡配置 vi etc network interfaces 结果发现只
  • Circular placeholder reference 'server.port' in property definitions

    Exception in thread main java lang IllegalArgumentException Circular placeholder reference server port in property defin
  • Cannot run program "scripts\saveVersion.sh"

    用Maven 编译hadoop遇到以下错误 saveVersion sh script fails in windows cygwin hadoop yarn common 半天是个bug 解决方案如下 Index hadoop mapre
  • C++常用经典算法总结

    一 算法概述 排序算法可以分为两大类 非线性时间比较类排序 通过比较来决定元素间的相对次序 由于其时间复杂度不能突破O nlogn 因此称为非线性时间比较类排序 线性时间非比较类排序 不通过比较来决定元素间的相对次序 它可以突破基于比较排序
  • C#如何通过存储过程从数据库中获得数据

    存储过程就是在数据库中写好的函数 C 通过调用存储过程来获得数据 可以在一定程度上提高数据库的安全性 将一些重要的数据封装了起来 那么如何在C 中调用存储过程呢 一 存储过程 环境如下 1 数据库Itcast2014中包含表TblStude
  • VS的C++项目添加LAPACK库简便方法(注:64位+32位,且不用自己编译库)

    需要材料 1 已经编译好的库文件 dll文件和头文件 http icl cs utk edu lapack for windows lapack libraries 这个网站中有已经用minGW编译好的LAPACK库 lib 一共有三个 除
  • 实践DIV+CSS网页布局入门指南

    实践DIV CSS网页布局入门指南 你正在学习CSS布局吗 是不是还不能完全掌握纯CSS布局 通常有两种情况阻碍你的学习 第一种可能是你还没有理解CSS处理页面的原理 在你考虑你的页面整体表现效果前 你应当先考虑内容的语义和结构 然后再针对
  • uniapp使用jsZip打包多个url文件,下载为一个压缩包

    1 需求及前言 可选中多个文件 类型不限png doc xls ppt等 点击下载时 将选中的文件全部打包成一个压缩包给用户 本文讲解jszip这个插件的打包下载使用方法 2 下载插件 npm install file saver npm
  • kafka服务端常见报错

    打印错误ERROR日志 cat kafkaserver log grep i A3 ERROR 日志目录 1 x data 2 x data logs kedacom project namespace dol kafka dol kafk
  • c++四内存区

    c 程序执行时 内存分为四个区域 1 代码区 存放函数体的二进制代码 由操作系统管理 2 全局区 存放全局变量 静态变量和常亮 3 栈区 编译器自动分配释放 存放函数的参数和局部变量等 4 堆区 程序员分配和释放 若未释放 程序结束时有操作
  • # 关于idea中模块文件夹右下角没有蓝色小方块,pom文件显示橘色

    关于idea中模块文件夹右下角没有蓝色小方块 pom文件显示橘色 模块文件夹中右下角没有蓝色小方块 根本原因是因为模块文件夹中没有xxx iml文件 这个本人亲自试验过 将xxx iml文件删除后 模块文件夹右下角小蓝块立马消失 可以参考下
  • 玩好go的切片

    go的slice 入门就会遇到 但这个东西大多数人都是停留在简单的使用 一些干了好几年的老程序员都说不明白里面的道道 这里面坑不少 恰巧今天有空 好好整理下 永不踩坑 1 为什么要用切片 其他语言大多用的都是数组 在go中 数组的长度是不可