Go语言 & 与 * 取值赋值以及函数入参的区别

在 Go 中函数可以接受值传递和指针传递,使用时就涉及到 & 内存地址(指针)与 * 指针赋值的使用,它们的区别是什么?在实际业务使用中,值传递和指针传递的分别应对什么场景需要?针对使用时机进行分析。

所属分类 Golang

相关标签 变量内存指针

&和*

Go 语言中,一个变量可以正常赋值。

通过 & 获取内存地址(指针),指针也可以赋值。

这使得我这种从 Java 语言跳出的小伙伴看的一头雾水,先看概念。

  • &:取址符(指针),用于返回一个变量的内存地址。
  • *:指针值,用于返回一个指针的具体变量值。

编写一个简单的实例来感受下变量和指针的特性。

func main() {
    A := 3
    fmt.Printf("A值:%v 
", A)
    fmt.Printf("A内存地址:%p 
", &A)
    // 变量取址(指针)
    P := &A
    fmt.Printf("P值(实际是A的地址):%v 
", P)
    fmt.Printf("P内存地址:%p 
", &P)
    // 指针取值
    fmt.Printf("P实际值:%v 
", *P)
}

输出结果如下:

A值:3
A内存地址:0xc00000a0c8
P值(实际是A的地址):0xc00000a0c8
P内存地址:0xc000006030
P实际值:3

直接上代码。

func main() {
    A := 3
    fmt.Printf("A值:%v 
", A)
    P := &A
    fmt.Printf("P值(实际是A的地址):%v 
", P)
    fmt.Printf("P实际值:%v 
", *P)
    // 变量赋值
    A = 4
    fmt.Printf("A值:%v 
", A)
    // 指针赋值
    *P = 5
    fmt.Printf("P值(实际是A的地址):%v 
", P)
    fmt.Printf("P实际值:%v 
", *P)
    fmt.Printf("A值:%v 
", A)
}

输出结果如下:

A值:3
P值(实际是A的地址):0xc000062090
P实际值:3
A值:4
P值(实际是A的地址):0xc000062090
P实际值:5
A值:5

通过变量赋值和指针赋值,可得大致得到如下流程图:

image-20210713104110485

函数入参

从上文中,我们可以看到变量的值修改可以使用变量赋值或者指针赋值。

表面上看起来,最终实现的效果都一致,那指针赋值的意义何在呢?感觉似乎绕了一圈实现了同一个结果?

这里就涉及到函数入参问题,在 Go 中函数的入参均是值传递,也就是说将入参对象(变量)的值 Copy 一份出来传到函数中。

既然是值传递就必然带来一个问题,无法修改入参变量原本的值,可以通过下方 DEMO 验证一下。

func main() {
    A := 3
    fmt.Printf("A值:%v 
", A)
    // A 值修改方法
    changeA(A)
    fmt.Printf("修改方法后的A值:%v 
", A)
}

func changeA(A int) {
    A = 4
    fmt.Printf("修改方法,A值:%v 
", A)
}

输出结果如下:

A值:3
修改方法,A值:4
修改方法后的A值:3

为了进一步验证为什么修改没有生效,我们输出各自的内存地址:

A值:3
A内存地址:0xc00000a0c8
修改方法,A内存地址:0xc00000a110
修改方法,A值:4
修改方法后的A内存地址:0xc00000a0c8
修改方法后的A值:3

我们发现,在 changeA() 函数中所修改的 A 变量的内存地址和 main() 函数的内存地址并不一样,因此可以确定函数中的 A 变量是一个全新的 Copy 出来的变量。

如何解决?

常见的形式就是函数直接返回修改后的变量。

func main() {
    A := 3
    fmt.Printf("A值:%v 
", A)
    // A 值修改方法
    A = changeA(A)
    fmt.Printf("修改方法后的A值:%v 
", A)
}

func changeA(A int) int {
    A = 4
    fmt.Printf("修改方法,A值:%v 
", A)
    return A
}

输出结果如下:

A值:3
修改方法,A值:4
修改方法后的A值:4

如果我们不想函数有返回值,又希望修改入参变量的值,指针和指针赋值就可以达成这个目录。

func main() {
    A := 3
    fmt.Printf("A值:%v 
", A)
    // A 值修改方法
    changeA(&A)
    fmt.Printf("修改方法后的A值:%v 
", A)
}

func changeA(A *int) {
    *A = 4
    fmt.Printf("修改方法,A值:%v 
", A)
    fmt.Printf("修改方法,A实际值:%v 
", *A)
}

输出结果如下:

A值:3
修改方法,A值:0xc00000a0c8
修改方法,A实际值:4
修改方法后的A值:4

入参形式选择

从上文我们大致感受到,函数在接受参数时均使用值传递,一定会执行一次 Copy 的过程。

如果直接 Copy 的是一个对象(变量),对 Copy 后的对象(变量)修改永远无法影响初始对象(变量),除非返回结果并对初始对象(变量)重新赋值。

如果 Copy 的是一个指针,本质上在对 Copy 后的指针赋值的时候,最终还是指向了初始对象(变量),因此能够实现初始对象(变量)的修改。

既然如此,在什么场景下选择什么入参形式就一目了然了。

  1. 单纯的基于入参梳理后续数据,可以优先考虑直接对象(变量)传递。
  2. 涉及到对初始对象(变量)进行修改,可以考虑使用指针传递。
  3. 如果初始对象(变量)是基本数据类型或者简单类型的 struct,建议直接变量传递,通过返回值赋值,效率较高(非指针内存多数放在栈上)。
  4. 如果初始对象(变量)比较复杂,体量大(Copy 内存消耗高)或者涉及多层的嵌套(深层赋值的消耗),建议使用指针传递。

米虫

做一个有理想的米虫,伪全栈程序猿,乐观主义者,坚信一切都是最好的安排!

本站由个人原创、收集或整理,如涉及侵权请联系删除

本站内容支持转发,希望贵方携带转载信息和原文链接

本站具有时效性,不提供有效、可用和准确等相关保证

本站不提供免费技术支持,暂不推荐您使用案例商业化

发表观点

提示

昵称

邮箱

QQ

网址

当前还没有观点发布,欢迎您留下足迹!

同类其他

Golang

GOPROXY依赖包代理设置

Go1.11版本开始支持包依赖管理工具,新增了GOPROXY环境变量,用于配置依赖包下载代理,通过代理配置可以实现翻墙下载一些所需的依赖包,可以说相当实用

Go数据类型rune介绍和使用

Go 中比较常见的 int、string、bool、float 基本数据类型之外还有其他的数据类型可以应用在特殊场景,比如 rune 就是类似于 int32,因为其可表示的字符范围更大,实际工作中可以用来计算字符串的真实长度

GoLand中旧工程导入飘红处理思路

对于绝大多数新的 Go 项目而言,因为使用 go modules 管理包依赖从而无需要关注工程的目录位置,但是对于一些旧/历史工程在导入 GoLand 之后会出现全面飘红,这个时候就需要逐一排查问题

GoLand设置gofmt和goimports代码格式化

GoLand 在保存代码时,可以自动调用 gofmt 和 goimports 实现自动格式化代码,在新版本中可以通过 File Watchers 插件来完成这些配置,配置位置位于File

Go报错xx is shadowed during return

调试 Go 程序报错 xx is shadowed during return,方法在返回的时候不是预期的返回结果,错误的产生应当是在相同作用域中出现了同名的变量导致,根据实际业务场景进行修改

Go中接口的设计与实现

Go语言中的接口采用的是隐式实现,不需要去申明实现,只需要直接实现接口所定义的全部方法即可,同时区分了直接实现与指针实现两种形态,在实际使用时需要注意和关注

选择个人头像

昵称

邮箱

QQ

网址

评论提示

  • 头像:系统为您提供了12个头像自由选择,初次打开随机为你选择一个
  • 邮箱:可选提交邮箱,该信息不会外泄,或将上线管理员回复邮件通知
  • 网址:可选提交网址,评论区该地址将以外链的形式展示在您的昵称上
  • 记忆:浏览器将记忆您已选择或填写过得信息,下次评论无需重复输入
  • 审核:提供一个和谐友善的评论环境,本站所有评论需要经过人工审核