【Go 进阶】Go 语言到底是值传递,还是引用传递?(二)

简介: 【Go 进阶】Go 语言到底是值传递,还是引用传递?(二)

2、函数参数中的 type

2.1 基本数据类型

来看一个例子:

package main
import "fmt"
func main() {
  a, b := 0, 0
  fmt.Printf("====== 初始化 ====== \n")
  fmt.Printf("Memory Location a: %p, b: %p\n", &a, &b)
  fmt.Printf("value a: %d, b: %d\n", a, b) // 0 0
  Add(a)
  AddPtr(&b)
  fmt.Printf("\n ======  final ====== \n")
  fmt.Printf("Memory Location a: %p, b: %p\n", &a, &b)
  fmt.Printf("value a: %d, b: %d\n", a, b) // 0 1
}
// 通过值传递
func Add(x int) {
  fmt.Printf("\n======   'Add' 函数 ====== \n")
  fmt.Printf("Before Add, Memory Location: %p, Value: %d\n", &x, x)
  x++
  fmt.Printf("After Add, Memory Location: %p, Value: %d\n", &x, x)
}
// 通过指针传递
func AddPtr(x *int) {
  fmt.Printf("\n ======  'AddPtr' 函数 ====== \n")
  fmt.Printf("Before AddPtr, Memory Location: %p, Value: %d\n", x, *x)
  *x++ // We add * in front of the variable because it is a pointer, * will call value of a pointer
  fmt.Printf("After AddPtr, Memory Location: %p, Value: %d\n", x, *x)
}

输出

====== 初始化 ====== 
Memory Location a: 0xc00010a000, b: 0xc00010a008
value a: 0, b: 0
======   'Add' 函数 ====== 
Before Add, Memory Location: 0xc00010a010, Value: 0
After Add,  Memory Location: 0xc00010a010, Value: 1
 ======  'AddPtr' 函数 ====== 
Before AddPtr, Memory Location: 0xc00010a008, Value: 0
After AddPtr,  Memory Location: 0xc00010a008, Value: 1
 ======  final ====== 
Memory Location a: 0xc00010a000, b: 0xc00010a008
value a: 0, b: 1

Add 方法中,内存位置的值与 main() 中的 a 不一样,因为 Go 复制了 a 的值并初始化了一个新的内存位置,所以如果我们改变值 x++a 仍然是 0。a 的最终输出是0,因为它是 Pass by value

在  AddPtr 方法中,值的内存位置与 b 相同,所以我们知道它共享同一个 "容器",我们在 AddPtr 中对 x 所做的一切都会影响 b 的值,我们尝试在 *x++ 中增加 x 的值。b 的最终输出是1,这是因为通过引用传递而改变的。

其他基本数据类型如 int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string, bool, byte, rune, Array, StructsArrayStruct 与基本数据类型具有相同的属性。

2.2 引用数据类型

2.2.1 map 类型

对于 Map 类型来说,一来我们可以通过函数修改它的内容,二来它没有明显的指针。

func main() {
    users := make(map[int]string)
    users[1] = "user1"
    fmt.Printf("before modify: user:%v\n", users[1])  // before modify: user:user1
    modify(users)
    fmt.Printf("after modify: user:%v\n", users[1])  // after modify: user:user2
}
func modify(u map[int]string) {
    u[1] = "user2"
}

我们都知道,值传递是一份拷贝,里面的修改并不影响外面实参的值,那为什么map在函数内部的修改可以影响外部呢?

通过查看源码我们可以看到,实际上make底层调用的是makemap函数,主要做的工作就是初始化hmap结构体的各种字段:

func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
    //...
}

通过查看src/runtime/hashmap.go源代码发现,make函数返回的是一个hmap类型的指针*hmap。也就是说map===*hmap。 现在看func modify(p map)这样的函数,其实就等于func modify(p *hmap),相当于传递了一个指针进来。

而对于指针类型的参数来说,只是复制了指针本身,指针所指向的地址还是之前的地址。所以对map的修改是可以影响到函数外部的。

2.2.2 chan 类型

chan类型本质上和map类型是一样的,这里不做过多的介绍,参考下源代码:

func makechan(t *chantype, size int64) *hchan {
    //...
}

chan也是一个引用类型,和map相差无几,make返回的是一个*hchan

2.2.3 slice 类型

mapchan 使用 make 函数返回的实际上是 *hmap*hchan 指针类型,也就是指针传递。slice 虽然也是引用类型,但是它又有点不一样。

简单来说就是,slice 本身是个结构体,但它内部第一个元素是一个指针类型,指向底层的具体数组,slice 在传递时,形参是拷贝的实参这个 slice,但他们底层指向的数组是一样的,拷贝 slice 时,其内部指针的值也被拷贝了,也就是说指针的内容一样,都是指向同一个数组。

我们先看一个简单的例子,对slice的某一元素进行赋值:

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

下面举个例子:

func main() {
    arr := make([]int, 0)
    arr = append(arr, 1, 2, 3)
    fmt.Printf("outer1: %p, %p\n", &arr, &arr[0])
    modify(arr)
    fmt.Println(arr)  // 10, 2, 3
}
func modify(arr []int) {
    fmt.Printf("inner1: %p, %p\n", &arr, &arr[0])
    arr[0] = 10
    fmt.Printf("inner2: %p, %p\n", &arr, &arr[0])
}
//输出:
//outer1: 0x14000112018, 0x14000134000
//inner1: 0x14000112030, 0x14000134000
//inner2: 0x14000112030, 0x14000134000
//[10 2 3]

因为slice是引用类型,指向的是同一个数组。可以看到,在函数内外,arr 本身的地址 &arr 变了,但是两个指针指向的底层数据,也就是&arr[0]数组首元素的地址是不变的。所以在函数内部的修改可以影响到函数外部,这个很容易理解。

再来看另外一个稍微复杂的例子,函数内部使用append。这个会稍微不一样。

func main() {
    arr := make([]int, 0)
    //arr := make([]int, 0, 5)
    arr = append(arr, 1, 2, 3)
    fmt.Printf("outer1: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr))
    //modify(arr)
    appendSlice(arr)
    fmt.Printf("outer2: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr))
    fmt.Println(arr)
}
func appendSlice(arr []int) {
    fmt.Printf("inner1: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr))
    //modify(arr)
    arr = append(arr, 1)
    fmt.Printf("inner2: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr))
    //modify(arr) //&arr[0]的地址是否相等,取决于初始化slice的时候的capacity是否足够
}

这个问题就相对复杂的多了。

分两种情况:

1)make slice 的时候没有分配足够的capacity

arr := make([]int, 0) 像这种写法,那么输出就是:

outer1: 0x14000114018, 0x1400012e000, len:3, capacity:3
inner1: 0x14000114030, 0x1400012e000, len:3, capacity:3
inner2: 0x14000114030, 0x1400012c060, len:4, capacity:6
outer2: 0x14000114018, 0x1400012e000, len:3, capacity:3
[1 2 3]

虽然函数内部append的结果同样不影响外部的输出,但是原理却不一样。
image.png
不同点:

  1. 在内部调用append的时候,由于cap 容量足够,所以不需要扩容,在原地址空间增加一个元素,底层数组的首元素地址相同。
  2. 回到函数外部,打印出来还是[1 2 3],是因为外层的len是3,所以只能打印3个元素,实际上第四个元素的地址上已经有数据了。只不过因为len为3,所以我们无法看到第四个元素。

那正确的 append 应该是怎么样的呢:

appendSlice(&arr)
func appendSlice(arr *[]int) {
    *arr = append(*arr, 1)
}

传指针进去,这样拷贝的就是这个指针,指针指向的对象,也就是slice本身,是不变的,我们使用*arr可以对slice进行操作。

2.3 小结

  • Go里面没有引用传递,Go语言是值传递
  • 如果需要函数内部的修改能影响到函数外部,那么就传指针。
  • map/channel 本身就是指针,是引用类型,所以直接传 map和channel 本身就可以。
  • slice 的赋值操作其实是针对 slice 结构体内部的指针进行操作,也是指针,可以直接传 slice 本身。
  • sliceappend 操作 同时需要修改结构体的 len/cap,类似于 struct,如果需要传递到函数外部,需要传指针。(或者使用函数返回值)
相关文章
|
1天前
|
存储 编译器 Go
Go语言学习12-数据的使用
【5月更文挑战第5天】本篇 Huazie 向大家介绍 Go 语言数据的使用,包含赋值语句、常量与变量、可比性与有序性
17 6
Go语言学习12-数据的使用
|
2天前
|
Java Go
一文带你速通go语言指针
Go语言指针入门指南:简述指针用于提升效率,通过地址操作变量。文章作者sharkChili是Java/CSDN专家,维护Java Guide项目。文中介绍指针声明、取值,展示如何通过指针修改变量值及在函数中的应用。通过实例解析如何使用指针优化函数,以实现对原变量的直接修改。作者还邀请读者加入交流群深入探讨,并鼓励关注其公众号“写代码的SharkChili”。
9 0
|
2天前
|
存储 缓存 Java
来聊聊go语言的hashMap
本文介绍了Go语言中的`map`与Java的不同设计思想。作者`sharkChili`是一名Java和Go开发者,同时也是CSDN博客专家及JavaGuide项目的维护者。文章探讨了Go语言`map`的数据结构,包括`count`、`buckets指针`和`bmap`,解释了键值对的存储方式,如何利用内存对齐优化空间使用,并展示了`map`的初始化、插入键值对以及查找数据的源码过程。此外,作者还分享了如何通过汇编查看`map`操作,并鼓励读者深入研究Go的哈希冲突解决和源码。最后,作者提供了一个交流群,供读者讨论相关话题。
10 0
|
3天前
|
Java Go
Go语言学习11-数据初始化
【5月更文挑战第3天】本篇带大家通过内建函数 new 和 make 了解Go语言的数据初始化过程
17 1
Go语言学习11-数据初始化
|
3天前
|
自然语言处理 安全 Java
速通Go语言编译过程
Go语言编译过程详解:从词法分析(生成token)到句法分析(构建语法树),再到语义分析(类型检查、推断、匹配及函数内联)、生成中间码(SSA)和汇编码。最后,通过链接生成可执行文件。作者sharkchili,CSDN Java博客专家,分享技术细节,邀请读者加入交流群。
22 2
|
4天前
|
Java Linux Go
一文带你速通Go语言基础语法
本文是关于Go语言的入门介绍,作者因其简洁高效的特性对Go语言情有独钟。文章首先概述了Go语言的优势,包括快速上手、并发编程简单、设计简洁且功能强大,以及丰富的标准库。接着,文章通过示例展示了如何编写和运行Go代码,包括声明包、导入包和输出语句。此外,还介绍了Go的语法基础,如变量类型(数字、字符串、布尔和复数)、变量赋值、类型转换和默认值。文章还涉及条件分支(if和switch)和循环结构(for)。最后,简要提到了Go函数的定义和多返回值特性,以及一些常见的Go命令。作者计划在后续文章中进一步探讨Go语言的其他方面。
10 0
|
5天前
|
JavaScript 前端开发 Go
Go语言的入门学习
【4月更文挑战第7天】Go语言,通常称为Golang,是由Google设计并开发的一种编程语言,它于2009年公开发布。Go的设计团队主要包括Robert Griesemer、Rob Pike和Ken Thompson,这三位都是计算机科学和软件工程领域的杰出人物。
13 1
|
5天前
|
Go
|
6天前
|
分布式计算 Java Go
Golang深入浅出之-Go语言中的分布式计算框架Apache Beam
【5月更文挑战第6天】Apache Beam是一个统一的编程模型,适用于批处理和流处理,主要支持Java和Python,但也提供实验性的Go SDK。Go SDK的基本概念包括`PTransform`、`PCollection`和`Pipeline`。在使用中,需注意类型转换、窗口和触发器配置、资源管理和错误处理。尽管Go SDK文档有限,生态系统尚不成熟,且性能可能不高,但它仍为分布式计算提供了可移植的解决方案。通过理解和掌握Beam模型,开发者能编写高效的数据处理程序。
134 1
|
6天前
|
算法 关系型数据库 MySQL
Go语言中的分布式ID生成器设计与实现
【5月更文挑战第6天】本文探讨了Go语言在分布式系统中生成全局唯一ID的策略,包括Twitter的Snowflake算法、UUID和MySQL自增ID。Snowflake算法通过时间戳、节点ID和序列号生成ID,Go实现中需处理时间回拨问题。UUID保证全局唯一,但长度较长。MySQL自增ID依赖数据库,可能造成性能瓶颈。选择策略时需考虑业务需求和并发、时间同步等挑战,以确保系统稳定可靠。
113 0