详解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 相关文章推荐
Golang 实现超大文件读取的两种方法
Apr 27 Golang
golang 实现两个结构体复制字段
Apr 28 Golang
golang 在windows中设置环境变量的操作
Apr 29 Golang
golang 实现Location跳转方式
May 02 Golang
go语言基础 seek光标位置os包的使用
May 09 Golang
详解Go语言Slice作为函数参数的使用
Jul 02 Golang
Go语言空白表示符_的实例用法
Jul 04 Golang
Go语言应该什么情况使用指针
Jul 25 Golang
Go语言安装并操作redis的go-redis库
Apr 14 Golang
Golang 并发编程 SingleFlight模式
Apr 26 Golang
Go语言入门exec的基本使用
May 20 Golang
GO中sync包自由控制并发示例详解
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中Cookie与Session的异同
2016/02/19 PHP
php版交通银行网银支付接口开发入门教程
2016/09/26 PHP
php实现多站点共用session实现单点登录的方法详解
2019/09/18 PHP
一端时间轮换的广告
2006/06/26 Javascript
一个用js实现控制台控件的代码
2007/09/04 Javascript
window.addeventjs事件驱动函数集合addEvent等
2008/02/19 Javascript
Js切换功能的简单方法
2010/11/23 Javascript
ASP.NET jQuery 实例1(在TextBox里面创建一个默认提示)
2012/01/13 Javascript
40个新鲜出炉的jQuery 插件和免费教程[上]
2012/07/24 Javascript
JS实现遮罩层效果的简单实例
2013/11/12 Javascript
优化Node.js Web应用运行速度的10个技巧
2014/09/03 Javascript
Javascript无参数和有参数类继承问题解决方法
2015/03/02 Javascript
javascript实现简单的贪吃蛇游戏
2015/03/31 Javascript
jQuery地图map悬停显示省市代码分享
2015/08/20 Javascript
jquery带有索引按钮且自动轮播切换特效代码分享
2015/09/15 Javascript
微信小程序 检查接口状态实例详解
2017/06/23 Javascript
Angular 1.x个人使用的经验小结
2017/07/19 Javascript
脚手架vue-cli工程webpack的基本用法详解
2018/09/29 Javascript
JavaScript基于数组实现的栈与队列操作示例
2018/12/22 Javascript
Vue路由前后端设计总结
2019/08/06 Javascript
Vue使用路由钩子拦截器beforeEach和afterEach监听路由
2020/11/16 Javascript
[42:27]DOTA2上海特级锦标赛主赛事日 - 3 败者组第三轮#2Fnatic VS OG第三局
2016/03/05 DOTA
Python3基础之基本运算符概述
2014/08/13 Python
在pytorch中实现只让指定变量向后传播梯度
2020/02/29 Python
python实现IOU计算案例
2020/04/12 Python
详解Django配置JWT认证方式
2020/05/09 Python
python实现学生信息管理系统源码
2021/02/22 Python
详解基于canvas的视频遮罩插件
2018/01/04 HTML / CSS
FirstCry阿联酋儿童和婴儿产品网上购物:FirstCry.ae
2021/02/22 全球购物
船舶专业个人求职信范文
2014/01/02 职场文书
简历上的自我评价怎么写
2014/01/28 职场文书
针对吵架老公保证书
2015/05/08 职场文书
花木兰观后感
2015/06/10 职场文书
Django使用channels + websocket打造在线聊天室
2021/05/20 Python
Python语言内置数据类型
2022/02/24 Python
Spring Data JPA框架Repository自定义实现
2022/04/28 Java/Android