详解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项目中使用init()
Apr 12 Golang
Go语言中的UTF-8实现
Apr 26 Golang
golang 实现两个结构体复制字段
Apr 28 Golang
解决golang在import自己的包报错的问题
Apr 29 Golang
go设置多个GOPATH的方式
May 05 Golang
完美解决golang go get私有仓库的问题
May 05 Golang
Go 自定义package包设置与导入操作
May 06 Golang
K8s部署发布Golang应用程序的实现方法
Jul 16 Golang
Go语言应该什么情况使用指针
Jul 25 Golang
使用GO语言实现Mysql数据库CURD的简单示例
Aug 07 Golang
GO语言异常处理分析 err接口及defer延迟
Apr 14 Golang
详解Go语言中Get/Post请求测试
Jun 01 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
火影忍者:三大瞳力之一的白眼,为什么没有写轮眼那么出色?
2020/03/02 日漫
基于php中使用excel的简单介绍
2013/08/02 PHP
php使用环形链表解决约瑟夫问题完整示例
2018/08/07 PHP
laravel5使用freetds连接sql server的方法
2018/12/07 PHP
php版本CKEditor 4和CKFinder安装及配置方法图文教程
2019/06/05 PHP
jquery插件制作 手风琴Panel效果实现
2012/08/17 Javascript
js获得鼠标的坐标值的方法
2013/03/13 Javascript
jQuery打印指定区域Html页面并自动分页
2014/07/04 Javascript
jQuery打字效果实现方法(附demo源码下载)
2015/12/18 Javascript
Bootstrap复选框和单选按钮美化插件(推荐)
2016/11/23 Javascript
JS在浏览器中解析Base64编码图像
2017/02/09 Javascript
Bootstrap modal 多弹窗之叠加显示不出弹窗问题的解决方案
2017/02/23 Javascript
Angular.js去除页面中显示的空行方法示例
2017/03/30 Javascript
easyui关于validatebox实现多重规则验证的方法(必看)
2017/04/12 Javascript
Vue中计算属性computed的示例解读
2017/07/26 Javascript
详谈innerHTML innerText的使用和区别
2017/08/18 Javascript
Node.js中Koa2在控制台输出请求日志的方法示例
2019/05/02 Javascript
JS实现简易计算器
2020/02/14 Javascript
如何使用three.js 制作一个三维的推箱子游戏
2020/07/29 Javascript
[02:22:36]《加油!DOTA》总决赛
2014/09/19 DOTA
[00:43]2016完美“圣”典风云人物:单车宣传片
2016/12/02 DOTA
python PyTorch预训练示例
2018/02/11 Python
python opencv将表格图片按照表格框线分割和识别
2019/10/30 Python
HTML5教程之html 5 本地数据库(Web Sql Database)
2014/04/03 HTML / CSS
Dr. Martens马汀博士官网:马丁靴始祖品牌
2016/10/15 全球购物
工程招投标邀请书
2014/01/26 职场文书
周年庆典主持词
2014/04/02 职场文书
寄语学生的话
2014/04/10 职场文书
财务会计专业自荐书
2014/06/30 职场文书
高一军训的心得体会
2014/09/01 职场文书
预备党员入党感言
2015/08/01 职场文书
2016年入党心得体会范文
2016/01/23 职场文书
浅谈MySQL之浅入深出页原理
2021/06/23 MySQL
如何利用opencv判断两张图片是否相同详解
2021/07/07 Python
Python基础数据类型tuple元组的概念与用法
2021/08/02 Python
Tomcat安装使用及部署Web项目的3种方法汇总
2022/08/14 Servers