详解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 interface判断为空nil的实现代码
Apr 24 Golang
Go语言切片前或中间插入项与内置copy()函数详解
Apr 27 Golang
go 原生http web 服务跨域restful api的写法介绍
Apr 27 Golang
golang 如何用反射reflect操作结构体
Apr 28 Golang
解决go在函数退出后子协程的退出问题
Apr 30 Golang
golang协程池模拟实现群发邮件功能
May 02 Golang
go设置多个GOPATH的方式
May 05 Golang
Go timer如何调度
Jun 09 Golang
Golang原生rpc(rpc服务端源码解读)
Apr 07 Golang
Go语言的协程上下文的几个方法和用法
Apr 11 Golang
Go语言 详解net的tcp服务
Apr 14 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版(3)
2006/10/09 PHP
海河写的 Discuz论坛帖子调用js的php代码
2007/08/23 PHP
PHP 输出缓存详解
2009/06/20 PHP
Zend Framework自定义Helper类相关注意事项总结
2016/03/14 PHP
PHP使用SWOOLE扩展实现定时同步 MySQL 数据
2017/04/09 PHP
PHP简单实现遍历目录下特定文件的方法小结
2017/05/22 PHP
Laravel框架模板继承操作示例
2018/06/11 PHP
PHP商品秒杀问题解决方案实例详解【mysql与redis】
2019/07/22 PHP
PHP设计模式入门之迭代器模式原理与实现方法分析
2020/04/26 PHP
如何在PHP中使用AES加密算法加密数据
2020/06/24 PHP
JavaScript DOM 学习第二章 编辑文本
2010/02/19 Javascript
JS实现横向与竖向两个选项卡Tab联动的方法
2015/09/27 Javascript
Jquery和JS获取ul中li标签的实现方法
2016/06/02 Javascript
JavaScript数组复制详解
2017/02/02 Javascript
AngularJS路由实现页面跳转实例
2017/03/03 Javascript
Vue实现typeahead组件功能(非常靠谱)
2017/08/26 Javascript
Vue中 key keep-alive的实现原理
2018/09/18 Javascript
JS学习笔记之数组去重实现方法小结
2019/05/29 Javascript
vue 使用axios 数据请求第三方插件的使用教程详解
2019/07/05 Javascript
Python基于PyGraphics包实现图片截取功能的方法
2017/12/21 Python
Python使用itertools模块实现排列组合功能示例
2018/07/02 Python
对python内置map和six.moves.map的区别详解
2018/12/19 Python
对python多线程SSH登录并发脚本详解
2019/02/14 Python
python 实现音频叠加的示例
2020/10/29 Python
详解Pycharm第三方库的安装及使用方法
2020/12/29 Python
css3中用animation的steps属性制作帧动画
2019/04/25 HTML / CSS
Max&Co官网:意大利年轻女性时尚品牌
2017/05/16 全球购物
Mio Skincare法国官网:身体紧致及孕期身体护理
2018/04/04 全球购物
几道PHP的面试题
2012/05/19 面试题
创立科技Java面试题
2015/11/29 面试题
手术室护士自我鉴定
2013/10/14 职场文书
房地产销售计划书
2014/01/10 职场文书
财务管理职业生涯规划书
2014/02/26 职场文书
2014年人事科工作总结
2014/11/19 职场文书
Go语言基础切片的创建及初始化示例详解
2021/11/17 Golang
python绘制简单直方图(质量分布图)的方法
2022/04/21 Python