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

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

本文是《GO 进阶》系列第一篇 ~

Go 语言里有指针的概念,它比 C++ 的指针要简单的多,同时你需要记住一个概念:Go 语言是 值传递。我们今天探讨的是在编码的时候到底该使用指针呢还是值类型?在作为参数和返回值的时候该如何去使用?两种传递方式有什么区别?

要搞懂这些问题,需要对 “Go语言是值传递” 这句话有深刻的理解。

1、Go 语言是值传递

先说结论,Go里面没有引用传递,Go语言是值传递。很多技术博客说Go语言有引用传递,都是没真的理解Go语言。而Go语言中的一些让你觉得它是引用传递的原因,是因为Go语言有值类型引用类型,但是它们都是值传递

  • 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  • 引用传递:指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

1.1 值类型和引用类型

可能对初学者来说,可能会搞混值类型和值传递,引用类型和引用传递。为了避免这种低级错误,先来了解一下 Go 语言中的值类型和引用类型:

  • 值类型:变量直接存储值,内存通常在栈上分配,栈在函数调用完会被释放。比如:intfloatboolstringarraysturct 等。
  • 引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。比如:slicemapchannelinterfacefunc 等。
  • 严格来说,Go 语言没有引用类型,但是我们可以把 map、chan、func、interface、slice 称为引用类型,这样便于理解。
  • 指针类型也可以理解为是一种引用类型

这里提到了堆和栈,简单介绍下内存分配中的堆和栈:

  • (操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • (操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

image.png这幅图中展示了常用的值类型和引用类型(注意:引用类型和传引用是两个概念)。在左边是我们常用的一些值类型,函数调用时需要使用指针修改底层数据;而右边是 “引用类型”,我们可以理解为它们的底层都是指针类型,所以右边的类型在使用的时候会有些不同。

所以在 Go 语言中:

  • 引用类型作为参数时,称为浅拷贝,形参改变,实参数跟随变化。因为传递的是地址,形参和实参都指向同一块地址
  • 值类型作为参数时,称为深拷贝,形参改变,实参不变,因为传递的是值的副本,形参会新开辟一块空间,与实参指向不同
  • 如果希望值类型数据在修改形参时实参跟随变化,可以把参数设置为指针类型

1.2 类型的零值

  1. 在 Go 语言中,定义变量可以通过声明或者通过 makenew函数,区别是 make 和 new 函数属于显示声明并初始化。
  2. 如果我们声明的变量没有显示的声明初始化,那么该变量的默认值就是对于类型的零值。
类型 零值
数值类型(int、float等) 0
bool false
string ""(空字符串)
struct 内部字段的零值
slice nil
map nil
指针 nil
func nil
chan nil
interface nil

1.3 值传递

一定要记住,在 Go 语言中,函数的参数传递只有值传递,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;如果拷贝的内容是指针(或者可以理解为引用类型 mapchan 等),那么就可以在函数中修改原始数据。

记住!Go 语言值传递! 可以看官网解释:When are function parameters passed by value?

When are function parameters passed by value?

As in all languages in the C family, everything in Go is passed by value. That is, a function always gets a copy of the thing being passed, as if there were an assignment statement assigning the value to the parameter. For instance, passing an int value to a function makes a copy of the int, and passing a pointer value makes a copy of the pointer, but not the data it points to. (See a later section for a discussion of how this affects method receivers.)

Map and slice values behave like pointers: they are descriptors that contain pointers to the underlying map or slice data. Copying a map or slice value doesn't copy the data it points to. Copying an interface value makes a copy of the thing stored in the interface value. If the interface value holds a struct, copying the interface value makes a copy of the struct. If the interface value holds a pointer, copying the interface value makes a copy of the pointer, but again not the data it points to.

大致意思:像 C 家族中的其他所有语言一样,Go 语言中的所有传递都是传值。也就是说,函数接收到的永远都是参数的一个副本,就好像有一条将值赋值给参数的赋值语句一样。例如,传递一个 int 值给一个函数,函数收到的是这个 int 值的副本,传递指针值,获得的是指针值的副本,而不是指针指向的数据。

参考:Should I define methods on values or pointers?,来了解这种方式对方法接收者的影响。

Map 和 Slice 的值表现和指针一样:它们是对内部映射或者切片数据的指针的描述符。复制 Map 和 Slice 的值,不会复制它们指向的数据。复制接口的值,会产生一个接口值存储的数据的副本。如果接口值存储的是一个结构体,复制接口值将产生一个结构体的副本。如果接口值存储的是指针,复制接口值会产生一个指针的副本,而不是指针指向的数据的副本。

1.4 一个典型的例子

理论讲完了,下面来看一个典型的值传递的例子:

package main
import (
  "fmt"
)
type student struct {
  name string
  age  int
}
func main() {
  i := 1
  str := "hello"
  stu := student{name: "iankevin", age: 18}
  test_demo(i, str, stu)
  fmt.Println(i, str, stu.age) // 1 hello 18
}
func test_demo(i int, str string, stu student) {
  i = 10
  str = "world"
  stu.age = 22
}

可以发现,虽然在函数里面对三个类型的变量都做了修改,但是并不会影响函数外的变量的值。那如果我们希望函数内的变量修改能影响到函数外的变量的值,怎么办呢?

答案是:传指针

因为传指针的值传递,复制的是指针本身,意味着形参和实参地址是一样的。所以我们在函数内部的修改,就能影响到函数外的变量的值。

package main
import (
  "fmt"
)
type student struct {
  name string
  age  int
}
func main() {
  i := 1
  str := "hello"
  stu := &student{name: "iankevin", age: 18}
        // 注意这里的 i 和 str 要取地址入参
  test_demo(&i, &str, stu)
  fmt.Println(i, str, stu.age) // 10 world 22
}
func test_demo(i *int, str *string, stu *student) {
        // 注意这里的 i 和 str 传入的是指针,所以要先取值(解引用)再赋值
  *i = 10
  *str = "world"
  stu.age = 22
}

注意,这可不是引用传递,只是因为我们传入的是指针,指针本身是一份拷贝,但是对这个指针解引用之后,也就是指针所指向的具体地址是一样的,所以函数内部对形参的修改,是会影响实参的。

Go 中是值传递,一个方法 / 函数总是获取这个传递的拷贝,只是有一个分配声明给这个参数分配这个数值。拷贝一个指针的值就做了这个指针的拷贝,而不是指针指向的数据(重点理解)。

相关文章
|
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的强大功能助力高效开发和运维。
22 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