【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天前
|
安全 Java Go
探索Go语言在高并发环境中的优势
在当今的技术环境中,高并发处理能力成为评估编程语言性能的关键因素之一。Go语言(Golang),作为Google开发的一种编程语言,以其独特的并发处理模型和高效的性能赢得了广泛关注。本文将深入探讨Go语言在高并发环境中的优势,尤其是其goroutine和channel机制如何简化并发编程,提升系统的响应速度和稳定性。通过具体的案例分析和性能对比,本文揭示了Go语言在实际应用中的高效性,并为开发者在选择合适技术栈时提供参考。
|
5天前
|
运维 Kubernetes Go
"解锁K8s二开新姿势!client-go:你不可不知的Go语言神器,让Kubernetes集群管理如虎添翼,秒变运维大神!"
【8月更文挑战第14天】随着云原生技术的发展,Kubernetes (K8s) 成为容器编排的首选。client-go作为K8s的官方Go语言客户端库,通过封装RESTful API,使开发者能便捷地管理集群资源,如Pods和服务。本文介绍client-go基本概念、使用方法及自定义操作。涵盖ClientSet、DynamicClient等客户端实现,以及lister、informer等组件,通过示例展示如何列出集群中的所有Pods。client-go的强大功能助力高效开发和运维。
24 1
|
5天前
|
SQL 关系型数据库 MySQL
Go语言中使用 sqlx 来操作 MySQL
Go语言因其高效的性能和简洁的语法而受到开发者们的欢迎。在开发过程中,数据库操作不可或缺。虽然Go的标准库提供了`database/sql`包支持数据库操作,但使用起来稍显复杂。为此,`sqlx`应运而生,作为`database/sql`的扩展库,它简化了许多常见的数据库任务。本文介绍如何使用`sqlx`包操作MySQL数据库,包括安装所需的包、连接数据库、创建表、插入/查询/更新/删除数据等操作,并展示了如何利用命名参数来进一步简化代码。通过`sqlx`,开发者可以更加高效且简洁地完成数据库交互任务。
13 1
|
5天前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
6天前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
3天前
|
NoSQL Go Redis
Go语言中如何扫描Redis中大量的key
在Redis中,遍历大量键时直接使用`KEYS`命令会导致性能瓶颈,因为它会一次性返回所有匹配的键,可能阻塞Redis并影响服务稳定性。为解决此问题,Redis提供了`SCAN`命令来分批迭代键,避免一次性加载过多数据。本文通过两个Go语言示例演示如何使用`SCAN`命令:第一个示例展示了基本的手动迭代方式;第二个示例则利用`Iterator`简化迭代过程。这两种方法均有效地避免了`KEYS`命令的性能问题,并提高了遍历Redis键的效率。
10 0
|
4天前
|
监控 Serverless Go
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
|
4天前
|
关系型数据库 MySQL 数据库连接
Go语言中使用sqlx来操作事务
在应用中,数据库事务保证操作的ACID特性至关重要。`github.com/jmoiron/sqlx`简化了数据库操作。首先安装SQLX和MySQL驱动:`go get github.com/jmoiron/sqlx`和`go get github.com/go-sql-driver/mysql`。导入所需的包后,创建数据库连接并使用`Beginx()`方法开始事务。通过`tx.Commit()`提交或`tx.Rollback()`回滚事务以确保数据一致性和完整性。
8 0
|
6天前
|
SQL 安全 关系型数据库
Go 语言中的 MySQL 事务操作
在现代应用中,确保数据完整与一致至关重要。MySQL的事务机制提供了可靠保障。本文首先解释了事务的概念及其ACID特性,随后介绍了如何在Go语言中使用`database/sql`包进行MySQL事务操作。通过一个银行转账的例子,演示了如何通过Go开启事务、执行操作并在必要时回滚或提交,确保数据一致性。最后,还讨论了不同事务隔离级别的含义及如何在Go中设置这些级别。通过本文的学习,开发者能更好地掌握MySQL事务的应用。
11 0
|
7天前
|
SQL 关系型数据库 MySQL
Go语言中进行MySQL预处理和SQL注入防护
在现代Web应用开发中,安全性至关重要。SQL注入是一种常见的攻击方式,攻击者可通过构造特殊SQL查询来非法访问或修改数据库数据。本文介绍如何利用Go语言中的预处理SQL语句来防范此类攻击。预处理不仅能提升安全性,还能提高性能并简化代码。通过使用`?`作为占位符,Go自动处理参数转义,有效避免SQL注入。此外,文章还提供了连接MySQL数据库、执行预处理查询以及最佳实践的示例代码。务必遵循这些指导原则,确保应用程序的安全性。
16 0