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

相关文章
|
17天前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
123 4
|
1月前
|
Linux Go iOS开发
Go语言100个实战案例-进阶与部署篇:使用Go打包生成可执行文件
本文详解Go语言打包与跨平台编译技巧,涵盖`go build`命令、多平台构建、二进制优化及资源嵌入(embed),助你将项目编译为无依赖的独立可执行文件,轻松实现高效分发与部署。
|
17天前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
72 1
|
2月前
|
Cloud Native 安全 Java
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
254 1
|
2月前
|
Cloud Native Go API
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
297 0
|
2月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
205 0
|
2月前
|
Cloud Native Java 中间件
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
169 0
|
2月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
263 0
|
Java 编译器 Go
一起学Golang系列(五)初次接触Go语言可能遇到的各种坑!
前面介绍了Go语言的基础语法,所谓磨刀不误砍柴工,希望大家还是能熟悉掌握这些基础知识,这样后面真正学起Go来才会得心应手。 作为初学者。Go语言的语法有些和java类似,但也有很多不一样的地方。刚开始都会遇到各种各样的坑。下面就来总结下学习go语言的过程中,遇到的各种坑。
一起学Golang系列(五)初次接触Go语言可能遇到的各种坑!
|
8月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。

热门文章

最新文章