提出疑问
在Go的源码库或者其他开源项目中,会发现有些函数在需要用到切片入参时,它采用是指向切片类型的指针,而非切片类型。这里未免会产生疑问:切片底层不就是指针指向底层数组数据吗,为何不直接传递切片,两者有什么区别?
例如,在源码log包中,Logger
对象上绑定了formatHeader
方法,它的入参对象buf
,其类型是*[]byte
,而非[]byte
。
1func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {}
有以下例子
1func modifySlice(innerSlice []string) { 2 innerSlice[0] = "b" 3 innerSlice[1] = "b" 4 fmt.Println(innerSlice) 5} 6 7func main() { 8 outerSlice := []string{"a", "a"} 9 modifySlice(outerSlice)10 fmt.Print(outerSlice)11}1213// 输出如下14[b b]15[b b]
我们将modifySlice
函数的入参类型改为指向切片的指针
1func modifySlice(innerSlice *[]string) { 2 (*innerSlice)[0] = "b" 3 (*innerSlice)[1] = "b" 4 fmt.Println(*innerSlice) 5} 6 7func main() { 8 outerSlice := []string{"a", "a"} 9 modifySlice(&outerSlice)10 fmt.Print(outerSlice)11}1213// 输出如下14[b b]15[b b]
在上面的例子中,两种函数传参类型得到的结果都一样,似乎没发现有什么区别。通过指针传递它看起来毫无用处,而且无论如何切片都是通过引用传递的,在两种情况下切片内容都得到了修改。
这印证了我们一贯的认知:函数内对切片的修改,将会影响到函数外的切片。但,真的是如此吗?
考证与解释
在《你真的懂string与[]byte的转换了吗》一文中,我们讲过切片的底层结构如下所示。
1type slice struct {2 array unsafe.Pointer3 len int4 cap int5}
array
是底层数组的指针,len
表示长度,cap
表示容量。
我们对上文中的例子,做以下细微的改动。
1func modifySlice(innerSlice []string) { 2 innerSlice = append(innerSlice, "a") 3 innerSlice[0] = "b" 4 innerSlice[1] = "b" 5 fmt.Println(innerSlice) 6} 7 8func main() { 9 outerSlice := []string{"a", "a"}10 modifySlice(outerSlice)11 fmt.Print(outerSlice)12}1314// 输出如下15[b b a]16[a a]
神奇的事情发生了,函数内对切片的修改竟然没能对外部切片造成影响?
为了清晰地明白发生了什么,将打印添加更多细节。
1func modifySlice(innerSlice []string) { 2 fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0]) 3 innerSlice = append(innerSlice, "a") 4 innerSlice[0] = "b" 5 innerSlice[1] = "b" 6 fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0]) 7} 8 9func main() {10 outerSlice := []string{"a", "a"}11 fmt.Printf("%p %v %p\n", &outerSlice, outerSlice, &outerSlice[0])12 modifySlice(outerSlice)13 fmt.Printf("%p %v %p\n", &outerSlice, outerSlice, &outerSlice[0])14}1516// 输出如下170xc00000c060 [a a] 0xc00000c080180xc00000c0c0 [a a] 0xc00000c080190xc00000c0c0 [b b a] 0xc000022080200xc00000c060 [a a] 0xc00000c080
在Go函数中,函数的参数传递均是值传递。那么,将切片通过参数传递给函数,其实质是复制了slice
结构体对象,两个slice
结构体的字段值均相等。正常情况下,由于函数内slice
结构体的array
和函数外slice
结构体的array
指向的是同一底层数组,所以当对底层数组中的数据做修改时,两者均会受到影响。
但是存在这样的问题:如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。
为了让读者更清晰的认识到这一点,将上述过程可视化如下。
可以看到,当切片的长度和容量相等时,发生append,就会触发切片的扩容。扩容时,会新建一个底层数组,将原有数组中的数据拷贝至新数组,追加的数据也会被置于新数组中。切片的array指针指向新底层数组。所以,函数内切片与函数外切片的关联已经彻底斩断,它的改变对函数外切片已经没有任何影响了。
注意,切片扩容并不总是等倍扩容。为了避免读者产生误解,这里对切片扩容原则简单说明一下(源码位于src/runtime/slice.go
中的 growslice
函数):
切片扩容时,当需要的容量超过原切片容量的两倍时,会直接使用需要的容量作为新容量。否则,当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
到此,我们终于知道为什么有些函数在用到切片入参时,它需要采用指向切片类型的指针,而非切片类型。
1func modifySlice(innerSlice *[]string) { 2 *innerSlice = append(*innerSlice, "a") 3 (*innerSlice)[0] = "b" 4 (*innerSlice)[1] = "b" 5 fmt.Println(*innerSlice) 6} 7 8func main() { 9 outerSlice := []string{"a", "a"}10 modifySlice(&outerSlice)11 fmt.Print(outerSlice)12}1314// 输出如下15[b b a]16[b b a]
请记住,如果你只想修改切片中元素的值,而不会更改切片的容量与指向,则可以按值传递切片,否则你应该考虑按指针传递。
例题巩固
为了判断读者是否已经真正理解上述问题,我将上面的例子做了两个变体,读者朋友们可以自测。
测试一
1func modifySlice(innerSlice []string) { 2 innerSlice[0] = "b" 3 innerSlice = append(innerSlice, "a") 4 innerSlice[1] = "b" 5 fmt.Println(innerSlice) 6} 7 8func main() { 9 outerSlice := []string{"a", "a"}10 modifySlice(outerSlice)11 fmt.Println(outerSlice)12}
测试二
1func modifySlice(innerSlice []string) { 2 innerSlice = append(innerSlice, "a") 3 innerSlice[0] = "b" 4 innerSlice[1] = "b" 5 fmt.Println(innerSlice) 6} 7 8func main() { 9 outerSlice:= make([]string, 0, 3)10 outerSlice = append(outerSlice, "a", "a")11 modifySlice(outerSlice)12 fmt.Println(outerSlice)13}
测试一答案
1[b b a]2[b a]
测试二答案
1[b b a]2[b b]
你做对了吗?
原网址: 访问
创建于: 2021-03-10 12:06:07
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
java windows火焰图_mob64ca12ec8020的技术博客_51CTO博客 - 在windows下不可行,不知道作者是怎样搞的 监听SpringBoot 服务启动成功事件并打印信息_监听springboot启动完毕-CSDN博客 SpringBoot中就绪探针和存活探针_management.endpoint.health.probes.enabled-CSDN博客 u2u转换板 - 嘉立创EDA开源硬件平台 Spring Boot 项目的轻量级 HTTP 客户端 retrofit 框架,快来试试它!_Java精选-CSDN博客 手把手教你打造一套最牛的知识笔记管理系统! - 知乎 - 想法有重合-理论可参考 安宇雨 闲鱼 机械键盘 客制化 开贴记录 文本 linux 使用find命令查找包含某字符串的文件_beijihukk的博客-CSDN博客_find 查找字符串 ---- mac 也适用 安宇雨 打字音 记录集合 B站 bilibili 自行搭建 开坑 真正的客制化 安宇雨 黑苹果开坑 查找工具包maven pom 引用地 工具网站 Dantelis 介绍的玩轴入坑攻略 --- 关于轴的一些说法 --- 非官方 ---- 心得而已 --- 长期开坑更新 [本人问题][新开坑位]关于自动化测试的工具与平台应用 机械键盘 开团 网站记录 -- 能做一个收集的程序就好了 不过现在没时间 -- 信息大多是在群里发的 - 你要让垃圾佬 都去一个地方看难度也是很大的 精神支柱 [超级前台]sprinbboot maven superdesk-app 记录 [信息有用] [环境准备] [基本完成] [sebp/elk] 给已创建的Docker容器增加新的端口映射 - qq_30599553的博客 - CSDN博客 [正在研究] Elasticsearch, Logstash, Kibana (ELK) Docker image documentation elasticsearch centos 安装记录 及 启动手记 正式服务器 39 elasticsearch 问题合集 不断更新 6.1.1 | 6.5.1 两个版本 博客程序 - 测试 - bug记录 等等问题 laravel的启动过程解析 - lpfuture - 博客园 OAuth2 Server PHP 用 Laravel 搭建带 OAuth2 验证的 RESTful 服务 | Laravel China 社区 - 高品质的 Laravel 和 PHP 开发者社区 利用Laravel 搭建oauth2 API接口 附 Unauthenticated 解决办法 - 煮茶的博客 - SegmentFault 思否 使用 OAuth2-Server-php 搭建 OAuth2 Server - 午时的海 - 博客园 基于PHP构建OAuth 2.0 服务端 认证平台 - Endv - 博客园 Laravel 的 Artisan 命令行工具 Laravel 的文件系统和云存储功能集成 浅谈Chromium中的设计模式--终--Observer模式 浅谈Chromium中的设计模式--二--pre/post和Delegate模式 浅谈Chromium中的设计模式--一--Chromium中模块分层和进程模型 DeepMind 4 Hacking Yourself README.md update 20211011
Laravel China 简书 知乎 博客园 CSDN博客 开源中国 Go Further Ryan是菜鸟 | LNMP技术栈笔记 云栖社区-阿里云 Netflix技术博客 Techie Delight Linkedin技术博客 Dropbox技术博客 Facebook技术博客 淘宝中间件团队 美团技术博客 360技术博客 古巷博客 - 一个专注于分享的不正常博客 软件测试知识传播 - 测试窝 有赞技术团队 阮一峰 语雀 静觅丨崔庆才的个人博客 软件测试从业者综合能力提升 - isTester IBM Java 开发 使用开放 Java 生态系统开发现代应用程序 pengdai 一个强大的博主 HTML5资源教程 | 分享HTML5开发资源和开发教程 蘑菇博客 - 专注于技术分享的博客平台 个人博客-leapMie 流星007 CSDN博客 - 舍其小伙伴 稀土掘金 Go 技术论坛 | Golang / Go 语言中国知识社区
最新评论