玩好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的切片 的相关文章

  • go语言使用gin框架

    gin框架基础用法 package main import github com gin gonic gin net http func main router gin Default router LoadHTMLGlob templat
  • Go语言函数

    http www jb51 net article 56831 htm Go语言中的函数有系统函数和自定义函数 1 系统函数 系统函数就是Go语言自带的函数 系统函数一般根据功能封装在不同的包内 比如Print Printf Println
  • Go基础(复杂类型):函数的闭包

    函数的闭包 Go 函数可以是一个闭包 闭包是一个函数值 它引用了其函数体之外的变量 该函数可以访问并赋予其引用的变量的值 换句话说 该函数被 绑定 在了这些变量上 例如 函数 adder 返回一个闭包 每个闭包都被绑定在其各自的 sum 变
  • go-redis 框架基本使用

    文章目录 redis使用场景 下载框架和连接redis 1 安装go redis 2 连接redis 字符串操作 有序集合操作 流水线 事务 1 普通事务 2 Watch redis使用场景 缓存系统 减轻主数据库 MySQL 的压力 计数
  • Go Web编程实战(7)----并发goroutine

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

    1 什么是类型断言 由于 interface 是 一般类型 不是具体类型 如果要转成具体类型 就需要使用类型断言 直接将 x 的值 赋给 a 是不可以的 编译前检查都过不去 断言后可成功赋值 输出结果为 5 2 带检查的类型断言 类型断言有
  • 微信小程序总结(2)- 需求分析

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

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

    我们有几个 Golang API 可以为 csvfiles 提供服务 但在提供以编程方式生成的 excel 文件方面没有任何帮助 为了避免重新编写 我们可以借助此服务器开始 main go 这使我们能够服务于路由 和 excel downl
  • 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的对象
  • error An unexpected error occurred: “https://registry.yarnpkg.com/axios: con 解决方案

    error An unexpected error occurred https registry yarnpkg com axios con 今天用在跑一个项目的时候发现了这个错误 看着像是网络连接不上 发现这里是用的Dokcerfile
  • go语言标准库

    在Go语言的安装文件里包含了一些可以直接使用的包 即标准库 Go语言的标准库 通常被称为语言自带的电池 提供了清晰的构建模块和公共接口 包含 I O 操作 文本处理 图像 密码学 网络和分布式应用程序等 并支持许多标准化的文件格式和编解码协
  • 解决GO语言编译程序在openwrt(mipsle架构)上运行提示Illegal instruction问题

    RT 最近在研究openwrt mipsle架构 上运行go语言编译出来的程序 一运行就报 Illegal instruction 这样的错误 百度和Google搜索了一遍 得出两种解决方案 PS 更新一遍 当时写这个文档的时候没有发现Go
  • golang json性能分析详解

    原文地址 https www jb51 net article 135264 htm json格式可以算我们日常最常用的序列化格式之一了 Go语言作为一个由Google开发 号称互联网的C语言的语言 自然也对JSON格式支持很好 下面这篇文
  • Go语言实现区块链与加密货币-Part1(基本原型、工作量证明、持久化)

    区块链 Blockchain 是21世纪最具革命性的技术之一 它仍然处于不断成长的阶段 而且还有很多潜力尚未显现 作为比特币的底层技术 它本质上只是一个分布式数据库 不过使它独一无二的是 区块链是一个公开的而不是私人的数据库 每个使用它的人
  • GO学习 --- 匿名函数

    一 匿名函数 Go支持匿名函数 如果我们某个函数只是希望使用一次 可以考虑使用匿名函数 匿名函数也可以实现多次调用 二 使用方式 方式一 在定义匿名函数时就直接调用 匿名函数 package main import fmt func mai
  • 【Go语言核心手册11】context.Context

    往期精选 欢迎转发 如何看待程序员35岁职业危机 Java全套学习资料 14W字 耗时半年整理 我肝了三个月 为你写出了GO核心手册 消息队列 从选型到原理 一文带你全部掌握 肝了一个月的ETCD 从Raft原理到实践 更多 11 1 内容
  • Go语言学习4-数组类型

    数组类型 引言 1 数组 1 1 类型表示法 1 2 值表示法 1 3 属性和基本操作 总结 引言 上篇我们了解 Go语言的基本数据类型 现在开始介绍数组类型 主要如下 1 数组 在Go语言中 数组被称为Array 就是一个由若干相同类型的
  • Go语言学习路线

    gogogo git 地址 Go 学习 学习路线 2 基础知识 3 开发工具安装地址 下载 Go基础知识 链接为gitee地址 放心查看 基础结构 learn1 go 基础语法 learn2 go 数据类型 learn3 go 变量 lea

随机推荐

  • 从零开始学习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中 数组的长度是不可