Go语言的传参和传引用[转]

简介:

目录[-]

传参和传引用的问题

很多非官方的文档和教材(包括一些已经出版的图书), 对Go语言的传参和引用的讲解 
都有很多问题. 导致众多Go语言新手对Go的函数参数传参有很多误解.

而传参和传引用是编程语言的根本问题, 如果这个问题理解错误可能会导致很多问题.

传slice不是传引用!

首先, Go语言的函数调用参数全部是传值的, 包括 slice/map/chan 在内所有类型, 没有传引用的说法.

具体请看Go语言的规范:

After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.

from: http://golang.org/ref/spec#Calls

什么叫传引用?

比如有以下代码:

var a Object
doSomething(a) // 修改a的值 print(a) 

如果函数doSomething修改a的值, 然后print打印出来的也是修改后的值, 
那么就可以认为doSomething是通过引用的方式使用了参数a.

为什么传slice不是传引用?

我们构造以下的代码:

func main() {
    a := []int{1,2,3} fmt.Println(a) modifySlice(a) fmt.Println(a) } func modifySlice(data []int) { data = nil } 

其中modifySlice修改了切片a, 输出结果如下:

[1 2 3]
[1 2 3] 

说明a在调用modifySlice前后并没有任何变化, 因此a必然是传值的!

为什么很多人误以为slice是传引用呢?

可能是FAQ说slice是引用类型, 但并不是传引用!

下面这个代码可能是错误的根源:

func main() {
    a := []int{1,2,3}
    fmt.Println(a) modifySliceData(a) fmt.Println(a) } func modifySliceData(data []int) { data[0] = 0 } 

输出为:

[1 2 3]
[0 2 3] 

函数modifySliceData确实通过参数修改了切片的内容.

但是请注意: 修改通过函数修改参数内容的机制有很多, 其中传参数的地址就可以修改参数的值(其实是修改参数中指针指向的数据), 并不是只有引用一种方式!

传指针和传引用是等价的吗?

比如有以下代码:

func main() {
    a := new(int) fmt.Println(a) modify(a) fmt.Println(a) } func modify(a *int) { a = nil } 

输出为:

0xc010000000
0xc010000000

可以看出指针a本身并没有变化. 传指针或传地址也只能修改指针指向的内存的值, 
并不能改变指针本身在值.

因此, 函数参数传传指针也是传值的, 并不是传引用!

所有类型的函数参数都是传值的!

包括slice/map/chan等基础类型和自定义的类型都是传值的.

但是因为slicemap/chan底层结构的差异, 又导致了它们传值的影响并不完全等同.

重点归纳如下:

  • GoSpec: the parameters of the call are passed by value!

  • map/slice/chan 都是传值, 不是传引用

  • map/chan 对应指针, 和引用类似

  • slice 是结构体和指针的混合体

  • slice 含 values/count/capacity 等信息, 是按值传递

  • slice 中的 values 是指针, 按值传递

  • 按值传递的 slice 只能修改values指向的数据, 其他都不能修改

  • 以指针或结构体的角度看, 都是值传递!

那Go语言有传引用的说法吗?

Go语言其实也是有传引用的地方的, 但是不是函数的参数, 而是闭包对外部环境是通过引用访问的.

查看以下的代码:

func main() {
    a := new(int) fmt.Println(a) func() { a = nil }() fmt.Println(a) } 

输出为:

0xc010000000
<nil>

因为闭包是通过引用的方式使用外部环境的a变量, 因此可以直接修改a的值.

比如下面2段代码的输出是截然不同的, 原因就是第二个代码是通过闭包引用的方式输出i变量:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i) // Output: 4 3 2 1 0 } fmt.Printf("\n") for i := 0; i < 5; i++ { defer func(){ fmt.Printf("%d ", i) } () // Output: 5 5 5 5 5 } 

像第二个代码就是于闭包引用导致的副作用, 回避这个副作用的办法是通过参数传值或每次闭包构造不同的临时变量:

// 方法1: 每次循环构造一个临时变量 i
for i := 0; i < 5; i++ {
    i := i
    defer func(){ fmt.Printf("%d ", i) } () // Output: 4 3 2 1 0 } // 方法2: 通过函数参数传参 for i := 0; i < 5; i++ { defer func(i int){ fmt.Printf("%d ", i) } (i) // Output: 4 3 2 1 0 } 

什么是引用类型, 和指针有何区别/联系 ?

在Go语言的官方FAQ中描述, maps/slices/channels 是引用类型, 数组是值类型:

Why are maps, slices, and channels references while arrays are values?

There's a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use. Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.

from: http://golang.org/doc/faq#references

我个人理解, 引用类型和指针在底层实现上是一样的. 
但是引用类型在语法上隐藏了显示的指针操作. 
引用类型和函数参数的传引用/传值并不是一个概念.

我们知道 maps/slices/channels 在底层虽然隐含了指针, 
但是使用中并没有需要使用指针的语法. 
但是引用内存毕竟是基于指针实现, 因此就必须依赖 make/new 之类的函数才能构造出来. 
当然它们都支持字面值语法构造, 但是本质上还是需要一个构造的过程的.

要用好Go语言的引用类型, 必须要了解一些底层的结构(特别是slice的混合结构).

我们可以自己给Go语言模拟一个引用类型. 
我们可以将值类型特定的数组类型定义为一个引用类型(同时提供一个构造函数):

type RefIntArray2 *[2]int

func NewRefIntArray2() RefIntArray2 {
    return RefIntArray2(new([2]int)) } 

这样我们就可以将 RefIntArray2 当作引用类型来使用.

func main() {
    refArr2 := NewRefIntArray2()
    fmt.Println(refArr2)
    modifyRefArr2(refArr2)
    fmt.Println(refArr2)
}

func modifyRefArr2(arr RefIntArray2) {
    arr[0] = 1 } 

输出为:

&[0 0]
&[1 0]

之所以选择数组作为例子, 是因为Go语言的数组指针可以直接用[]访问的语法糖. 
所以, 引用类型一般都是底层指针实现, 只是在上层加上的语法糖而已.

注: 本节根据 @hooluupog 和 @LoongWong 的评论做的补充.

总结

  • 函数参数传值, 闭包传引用!
  • slice 含 values/count/capacity 等信息, 是按值传递
  • 按值传递的 slice 只能修改values指向的数据, 其他都不能修改
  • slice 是结构体和指针的混合体
  • 引用类型和传引用是两个概念
 
from:http://my.oschina.net/chai2010/blog/161384
欢迎加群互相学习,共同进步。QQ群:iOS: 58099570 | Android: 572064792 | Nodejs:329118122 做人要厚道,转载请注明出处!















本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sunshine-anycall/p/4806930.html ,如需转载请自行联系原作者

相关文章
|
1天前
|
JSON 安全 Java
2024年的选择:为什么Go可能是理想的后端语言
【4月更文挑战第27天】Go语言在2024年成为后端开发的热门选择,其简洁设计、内置并发原语和强大工具链备受青睐。文章探讨了Go的设计哲学,如静态类型、垃圾回收和CSP并发模型,并介绍了使用Gin和Echo框架构建Web服务。Go的并发通过goroutines和channels实现,静态类型确保代码稳定性和安全性,快速编译速度利于迭代。Go广泛应用在云计算、微服务等领域,拥有丰富的生态系统和活跃社区,适合作为应对未来技术趋势的语言。
8 0
|
1天前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
9 2
|
1天前
|
存储 Go
Golang深入浅出之-Go语言依赖管理:GOPATH与Go Modules
【4月更文挑战第27天】Go语言依赖管理从`GOPATH`进化到Go Modules。`GOPATH`时代,项目结构混乱,可通过设置多个工作空间管理。Go Modules自Go 1.11起提供更现代的管理方式,通过`go.mod`文件控制依赖。常见问题包括忘记更新`go.mod`、处理本地依赖和模块私有化,可使用`go mod tidy`、`replace`语句和`go mod vendor`解决。理解并掌握Go Modules对现代Go开发至关重要。
7 2
|
1天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
7 1
|
1天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
8 1
|
1天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
5 0
|
1天前
|
Go C++
go 语言回调函数和闭包
go 语言回调函数和闭包
|
1天前
|
存储 负载均衡 监控
【Go 语言专栏】构建高可靠性的 Go 语言服务架构
【4月更文挑战第30天】本文探讨了如何利用Go语言构建高可靠性的服务架构。Go语言凭借其高效、简洁和并发性能,在构建服务架构中备受青睐。关键要素包括负载均衡、容错机制、监控预警、数据存储和服务治理。文章详细阐述了实现这些要素的具体步骤,通过实际案例分析和应对挑战的策略,强调了Go语言在构建稳定服务中的作用,旨在为开发者提供指导。
|
1天前
|
测试技术 Go 开发工具
【Go语言专栏】Go语言中的代码审查与最佳实践
【4月更文挑战第30天】Go语言因其简洁、高性能及并发能力,在云计算等领域广泛应用。代码审查对提升Go代码质量、遵循规范及团队协作至关重要。审查流程包括提交、审查、反馈、修改和合并代码。工具如GoLand、Git、ReviewBoard和GitHub提供支持。最佳实践包括遵循命名规范、添加注释、保持代码结构清晰、复用代码和确保测试覆盖。积极参与代码审查是提高质量的关键。
|
1天前
|
Prometheus 监控 Cloud Native
【Go语言专栏】Go语言中的日志记录与监控
【4月更文挑战第30天】Go语言在软件开发和运维中扮演重要角色,提供灵活的日志记录机制和与多种监控工具的集成。内置`log`包支持基本日志记录,而第三方库如`zap`、`zerolog`和`logrus`则扩展了更多功能。监控方面,Go应用可与Prometheus、Grafana、Jaeger等工具配合,实现系统指标收集、可视化和分布式追踪。健康检查通过HTTP端点确保服务可用性。结合日志和监控,能有效提升Go应用的稳定性和性能。