【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,如果需要传递到函数外部,需要传指针。(或者使用函数返回值)
相关文章
|
9天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
29 2
|
7天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
18 2
|
7天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
20 2
|
10天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
7天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
15 4
|
7天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
38 1
|
9天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
13天前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
107 53
|
12天前
|
Ubuntu 编译器 Linux
go语言中SQLite3驱动安装
【11月更文挑战第2天】
36 7
|
12天前
|
关系型数据库 Go 网络安全
go语言中PostgreSQL驱动安装
【11月更文挑战第2天】
45 5