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

相关文章
|
20天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
31 7
|
20天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
20天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
93 71
|
19天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
100 67
|
20天前
|
存储 Go
go语言中映射
go语言中映射
32 11
|
21天前
|
Go 索引
go语言修改元素
go语言修改元素
27 6
|
11天前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数
|
22天前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
91 62
|
24天前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
22天前
|
Go
go语言for遍历映射(map)
go语言for遍历映射(map)
31 12