详解Go语言Slice作为函数参数的使用


Posted in Golang onJuly 02, 2021
目录
  • 前言
  • 问题与解析
    • 典型问题
  • 其它疑问1
  • 其它疑问2
  • 结论
  • 参考链接

 

前言

首先要明确Go语言中实质只有值传递,引用传递和指针传递是相对于参数类型来说。

个人认为上诉的结论不对,把引用类型看做对指针的封装,一般封装为结构体,结构体是值类型,所以感觉都是值传递。不然我感觉其它语言实质不也都是值传递?不过我刚学Go,可能还没完全弄懂,这个有问题可以互相讨论下。

Go语言中的值类型:int、float、bool、array、sturct等,声明一个值类型变量时,编译器会在栈中分配一个空间,空间里存储的就是该变量的值。

Go语言中的引用类型:slice,map,channel,interface,func,string等,声明一个引用类型的变量,编译器会把实例的内存分配在堆上。

string和其他语言一样,是引用类型,string的底层实现struct String { byte* str; intgo len; }; 但是因为string不允许修改,每次操作string只能生成新的对象,所以在看起来使用时像值类型。

其实引用类型可以看作对指针的封装。

Slice切片在Go语言中实质是一种结构体类型,源码中定义如下:

源码位置:src/runtime/slice.go

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

从定义中我们可以知道slice是一种值类型,array是底层数组指针,它指向底层分配的数组;len是底层数组的元素个数;cap是底层数组的容量,超过容量会扩容。

 

问题与解析

 

典型问题

有了上面知识的铺垫,下面我们来看下把slice作为函数参数传递的典型问题:

package main

import "fmt"

func main() {
 tmp := make([]int, 0)
    fmt.Printf("%p\n", &tmp)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
 change(tmp)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}

func change(tmp []int) {
    fmt.Printf("%p\n", &tmp)
 tmp = append(tmp, 6)
    fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}
//运行结果
//0xc000004078
//[] 0 0 0x59cde0
//0xc0000040c0
//[6] 1 1 0xc000014098
//[] 0 0 0x59cde0

这是一个典型问题,你所有疑问的基本这种类型的问题。

疑问点:slice不是引用类型吗?把它做参数传递时实参应该同步修改啊,为什么main函数中的tmp没变?

解析:

从之前讲的知识中我们已经知道slice实质是一个结构体,其作为参数传递时形参实质复制了实参整个结构体的内容,其实就是值传递。

形参分配有一份内存空间,存放和实参相同的内容,从运行结果可以看出形参的内存地址和实参是不同的。

因为形参中底层数组指针和实参相同,所以当做修改操作时会同步修改到实参中,但是当使用append函数添加元素时,append函数返回的slice会覆盖修改到形参的内存空间中,和实参无关,所以在main函数中实参不变。可以在上面代码中看到函数中形参已变但实参未变。

有同学看到上面解析之后可能还会有一些疑问,比如:

append函数有扩容机制,当函数内使用append未扩容时,是不是就可以同步增加元素到实参中?
为什么传指针就可以和实参完全同步,指针不也和引用类似吗?
函数中使用append时,如果扩容,其中形参内存空间中底层数组的地址会被覆盖修改为新的扩容后的底层数组地址,而实参无变化。上面的代码就是如此。

 

其它疑问1

package main

import "fmt"

func main() {
 tmp := make([]int, 0, 5)
 tmp = append(tmp, 1, 2, 3)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
 change(tmp)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}

func change(tmp []int) {
 tmp = append(tmp, 4)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}
//[1 2 3] 3 5 0xc00000c300
//[1 2 3 4] 4 5 0xc00000c300
//[1 2 3] 3 5 0xc00000c300

疑问点:从代码中可以看出函数中使用append时是没有扩容的,因为形参中底层数组地址和实参是一致的,那为什么实参中没有增加元素?

解析:

其实实参中tmp[3]已经变为4,但是实参和形参内存空间中len和cap是独立的,形参中len修改为了4但实参中len仍然为3,所以实参中未增加元素。

关于tmp[3]已经变为4可以从如下代码中反映出来:

package main

import "fmt"

func main() {
 tmp := make([]int, 0, 5)
 tmp = append(tmp, 1, 2, 3, 4, 5)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
 change(tmp[:3])
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}

func change(tmp []int) {
 tmp = append(tmp, 6)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}
//[1 2 3 4 5] 5 5 0xc00000c300
//[1 2 3 6] 4 5 0xc00000c300
//[1 2 3 6 5] 5 5 0xc00000c300

可以看出实参中4已经变为6

或者从如下代码中更为直接的看出:

package main

import (
 "fmt"
 "unsafe"
)

func main() {
 tmp := make([]int, 0, 5)
 tmp = append(tmp, 1, 2, 3)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
 change(tmp)
 p := unsafe.Pointer(&tmp[2])
 q := uintptr(p) + 8
 t := (*int)(unsafe.Pointer(q))
 fmt.Println(*t)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}

func change(tmp []int) {
 tmp = append(tmp, 4)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}
//[1 2 3] 3 5 0xc00000c300
//[1 2 3 4] 4 5 0xc00000c300
//4
//[1 2 3] 3 5 0xc00000c300

用实参tmp[2]的地址往后移一个元素地址长度,得到tmp[3]的地址输出,可以看到变为了3。

 

其它疑问2

package main

import "fmt"

func main() {
 tmp := make([]int, 0, 5)
 tmp = append(tmp, 1, 2, 3)
 fmt.Printf("%p\n", &tmp)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
 change(&tmp)
 fmt.Printf("%v %d %d %p\n", tmp, len(tmp), cap(tmp), tmp)
}

func change(tmp *[]int) {
 *tmp = append(*tmp, 4)
 fmt.Printf("%p\n", tmp)
 fmt.Printf("%v %d %d %p\n", *tmp, len(*tmp), cap(*tmp), *tmp)
}
//0xc000004078
//[] 0 0 0xffdde0
//0xc000004078
//[1] 1 1 0xc000014098
//[1] 1 1 0xc000014098

疑问点:为什么指针可以同步修改到实参,*tmp = append(*tmp, 4)这不也是覆盖修改到形参吗?

解析:

首先明确传指针时传的是slice的地址,形参是地址而非一份和实参相同内容的内存空间,这点从代码中打印的0xc000004078地址可以看出。所以*tmp = append(*tmp, 4)这段代码覆盖修改的是0xc000004078这个地址指向的slice,即主函数中的tmp切片,这点从代码中主函数中切片tmp的底层数组地址从0xffdde0变为0xc000014098可以看出。

 

结论

当传指针时,对函数中slice的任何修改其实都是对主函数中slice的修改;当传引用,即slice本身时,对函数中slice使用append时的修改实际是对形参新分配内存空间的修改而实参不变,但当直接修改slice中值时能同步修改到实参中。

 

 

到此这篇关于详解Go语言Slice作为函数参数的使用的文章就介绍到这了,更多相关Go语言Slice函数参数内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Golang 相关文章推荐
go语言map与string的相互转换的实现
Apr 07 Golang
Golang 正则匹配效率详解
Apr 25 Golang
Golang中interface{}转为数组的操作
Apr 30 Golang
golang 实现时间戳和时间的转化
May 07 Golang
Go timer如何调度
Jun 09 Golang
基于Go语言构建RESTful API服务
Jul 25 Golang
修改并编译golang源码的操作步骤
Jul 25 Golang
golang 语言中错误处理机制
Aug 30 Golang
Go语言基础函数基本用法及示例详解
Nov 17 Golang
详解Go语言中Get/Post请求测试
Jun 01 Golang
Go 内联优化让程序员爱不释手
Jun 21 Golang
在ubuntu下安装go开发环境的全过程
Aug 05 Golang
golang 实用库gotable的具体使用
Jul 01 #Golang
试了下Golang实现try catch的方法
Jul 01 #Golang
Golang 语言控制并发 Goroutine的方法
Jun 30 #Golang
Golang的继承模拟实例
Jun 30 #Golang
go select编译期的优化处理逻辑使用场景分析
Go 语言下基于Redis分布式锁的实现方式
Jun 28 #Golang
go语言使用Casbin实现角色的权限控制
You might like
php计算2个日期的差值函数分享
2015/02/02 PHP
PHP获取指定时间段之间的 年,月,天,时,分,秒
2016/06/05 PHP
PHP常用字符串函数小结(推荐)
2018/08/05 PHP
Laravel 连接(Join)示例
2019/10/16 PHP
JavaScript在IE中“意外地调用了方法或属性访问”
2008/11/19 Javascript
js 图片等比例缩放代码
2010/05/13 Javascript
multiSteps 基于Jquery的多步骤滑动切换插件
2011/07/22 Javascript
window.opener用法和用途实例介绍
2013/08/19 Javascript
jquery实现的判断倒计时是否结束代码
2016/02/05 Javascript
vue,angular,avalon这三种MVVM框架优缺点
2016/04/27 Javascript
Javascript 动态改变imput type属性
2016/11/01 Javascript
HTML5实现微信拍摄上传照片功能
2017/04/21 Javascript
JS分页的实现(同步与异步)
2017/09/16 Javascript
AngularJS动态添加数据并删除的实例
2018/02/27 Javascript
工作中常用到的ES6语法
2018/09/04 Javascript
vue与原生app的对接交互的方法(混合开发)
2018/11/28 Javascript
详解NodeJS Https HSM双向认证实现
2019/03/12 NodeJs
vue+Element实现搜索关键字高亮功能
2019/05/28 Javascript
浅谈Vue为什么不能检测数组变动
2019/10/14 Javascript
es6数组之扩展运算符操作实例分析
2020/04/25 Javascript
jQuery实现推拉门效果
2020/10/19 jQuery
[48:24]完美世界DOTA2联赛循环赛LBZS vs Forest 第一场 10月30日
2020/10/31 DOTA
Python3访问并下载网页内容的方法
2015/07/28 Python
详解Python中dict与set的使用
2015/08/10 Python
python中numpy包使用教程之数组和相关操作详解
2017/07/30 Python
利用Pandas 创建空的DataFrame方法
2018/04/08 Python
Python cookbook(数据结构与算法)将多个映射合并为单个映射的方法
2018/04/19 Python
python如何使用socketserver模块实现并发聊天
2019/12/14 Python
详解Python中pyautogui库的最全使用方法
2020/04/01 Python
Python学习之time模块的基本使用
2021/01/17 Python
基于CSS3的CSS 多栏(Multi-column)实现瀑布流源码分享
2014/06/11 HTML / CSS
Europcar英国:英国汽车和货车租赁
2017/01/21 全球购物
巴西最大的珠宝连锁店:Vivara
2019/04/18 全球购物
有针对性的求职自荐信
2013/11/14 职场文书
北京奥运会主题口号
2014/06/13 职场文书
高考诚信考试承诺书
2015/04/29 职场文书