golang 函数参数传递--指针,引用和值(二)

这一章节我们来分析一下 golang 值,指针,引用的区别。在大学我们学习 C 语言对值和指针已经有足够了解了,但是引用这个概念是在更高级的语言中引入的,比如 java,引用和指针很像,但是它和指针有上面区别呢?为什么需要应用?。接下来我们通过一些示例一一了解他们。
在这里插入图片描述

可以理解为变量存储的内容,或者说变量所代表的存储空间的内容。值在函数中传递时会 copy 一个副本,也就是说传入函数后这个值和原来的变量就没有关系了,修改这个值不会影响原来变量的值,我们来看一个示例:

package main

import "fmt"

func main() {
    var a int = 1
    testfunc(a)
    fmt.Printf("a=%d\n", a)
}

func testfunc(a int) {
    a = a + 1
}

输出结果:

a=1

指针

在大学我们 C 语言的时候接触过指针的概念,golang 的指针和 C 语言的指针时一个含义,其内容是一段存储空间的地址。指针本身也是一个值,这不过这个值是一个存储空间的地址。然后函数参数传递的是一个指针,通过这个指针可以修改原来变量的值。我们把上面示例稍微修改一下在看看结果:

package main

import "fmt"

func main() {
    var a int = 1
    testfunc(&a)
    fmt.Printf("a=%d\n", a)
}

func testfunc(a *int) {
    *a = *a + 1
}

输出结果:

a=2

另外需要特性强调的是,golang 的指针是一个阉割版的指针,golang 的指针是不支持运算的,指针本身的值是无法改变。golang 这么做是为了防止出现野指针(指针指向了非法的空间)。

下面代码在编译时就会报错

package main

import "fmt"

func main() {
    var a int = 1
    testfunc(&a)
    fmt.Printf("a=%d\n", a)
}

func testfunc(a *int) {
    a = a + 1
}

报错信息

/main.go:12:10: cannot convert 1 (untyped int constant) to *int

其实 golang 的指针也并非是完全不能进行加减乘除运算的,但是需要先用 unsafe.pointer 把指针先转为整数,但是这目前不是本章的重点,后面我会写一篇专门的文档。

引用

这是我们这一章节重点要说的,应该有很多人对引用的理解始终不是那么的透彻,大家第一次接触引用的概念应该是在大学学习 java 的时候,其行为和指针很像但是无论老师和课本都强调它不是指针,那么引用到底和指针有哪些区别呢。其实引用的引入是为了处理一些复杂数据类型的,如 golang 中的 slice,map,chan 等,为什么说这些数据类型复杂呢?我们就以 slice(切片)举例说明。

先看下面程序:

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    fmt.Printf("value of a=%v; lenght of a=%d; capacity of a=%d\n", a, len(a), cap(a))
    b := a[1:3]
    fmt.Printf("value of b=%v; lenght of b=%d; capacity of b=%d\n", b, len(b), cap(b))
}

输出结果:

value of a=[1 2 3]; lenght of a=3; capacity of a=3
value of b=[2 3]; lenght of b=2; capacity of b=2

我先来解释一下为什么 slice 是一种复杂类型,然后再回来分析上面的结果。所谓复杂类型,就是这种类型的变量自带一下“内禀属性”或者叫“内建属性”有或者可以叫“元数据”,这些属性的并不需要人工赋予,是语言自动添加的,slice 有三个内禀属性:Data,Len,Cap,在 reflect 中其对应的底层的结构体如下:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
  • Data 是一个指针,指向了一段存储空间,这段存储空间用于存储 slice 数据的
  • Len 表示当前 slice 的长度
  • Cap 表示底层存储空间的容量

这样我们就很好理解上面的输出了:slice a 和 slice b 共用底层存储空间,所以它们的 Cap 属性是一样的。

现在我们在回来看引用,之所以会用引用是因为引用所代表的变量背后的数据结构是复杂的,有很多属性是语言自动处理的(语言不希望我们来改变这些数据),在这种情况下使用指针明显是不合适的。

下面这张图详细解释了 golang 中哪些类型变量是值类型,哪些类型变量是引用类型。

在这里插入图片描述

另外需要特别注意的是 slice 和 map 分别对应的有两个内建函数 append 和 delete,append 用于向 slice 追加数据但是当 slice 底层存储空间不足时 append 会把原 slice 的底层数据 copy 一份放入新的地址空间追加的数据放入新的地址空间,也就是说使用 append 不会改变原来的数据,我们来看一个例子:

package main

import (
    "fmt"
)

func main() {
    a := []int{1, 2}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a []int) {
    a = append(a, 3)
}

输出结果:

a=[1 2]

我们可以看到函数调用前后 a 的值没有变化。但是通过下面的方式改变函数里面 slice 的值,会影响到原来的变量:

package main

import (
    "fmt"
)

func main() {
    a := []int{1, 2}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a []int) {
    a[1] = 3
}

输出结果:

a=[1 3]

我接下来在看看 delete 是如何处理 map 的,直接看例子:

package main

import (
    "fmt"
)

func main() {
    a := map[int]string{1: "a", 2: "b"}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a map[int]string) {
    delete(a, 1)
}

输出结果:

a=map[2:b]

可以看到函数调用前后 map a 的值改变了,我们在来看一个向 map 中添加元素的例子:

package main

import (
    "fmt"
)

func main() {
    a := map[int]string{1: "a", 2: "b"}
    testfunc(a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a map[int]string) {
    a[3] = "c"
}

输出结果:

a=map[1:a 2:b 3:c]

同样的,map a 的值被改变了。

那么引用的指针呢,会有什么现象?我们在看一个函数参数是引用类型变量的指针的例子:

package main

import (
    "fmt"
)

func main() {
    a := []int{1, 2}
    testfunc(&a)
    fmt.Printf("a=%v\n", a)
}

func testfunc(a *[]int) {
    *a = append(*a, 3)
}

输出结果:

a=[1 2 3]

可以看到 slice a 在函数调用前后有变化。

总结

通过上面的例子大家能感受到 golang 数据类型的复杂,那么最后我就用一段话对上面这些现象及其原理做一下概括性解释。

golang 数据类型分为两大类:值类型和应用类型,如果函数参数是值类型,在函数调用时会 copy 一份数据,函数中改变数据不会改变原变量;如果函数参数时应用类型,在函数调用时只会 copy 引用本身并不会 copy 引用所代表的底层数据,但是在用 append 函数处理 slice 时,由于append 函数內部会 copy slice,所以通过 append 更改 slice 时不会影响原值,处理 map 的 delete 并不会 copy 数据所以会影响到原变量。对于指针,无论值类型的指针还是引用类型的指针都会改变原变量。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
个人学习golang笔记,从各种教程中总结而来,作为入门参考。目录如下 目录 1. 入门 1 1.1. Hello world 1 1.2. 命令行参数 2 2. 程序结构 3 2.1. 类型 4 2.1.1. 命名类型(named type)与未命名类型(unamed type) 4 2.1.2. 基础类型(underlying type) 4 2.1.3. 可赋性 5 2.1.4. 类型方法集 6 2.1.5. 类型声明 6 2.2. 变量 8 2.2.1. 变量声明 8 2.2.2. 类型零 12 2.2.3. 指针 13 2.3. 赋 17 2.4. 包和文件 17 2.5. 作用域 18 2.6. 语句 19 2.7. 比较运算符 20 2.8. 类型转换 21 2.9. 控制流 23 2.9.1. If 23 2.9.2. Goto 24 2.9.3. For 25 2.9.4. Switch 25 2.9.5. break语句 31 2.9.6. Continue语句 31 3. 基础数据类型 31 3.1. golang类型 31 3.2. Numeric types 32 3.3. 字符串 33 3.3.1. 什么是字符串 33 3.3.2. 字符串底层概念 35 3.3.3. 获取每个字节 38 3.3.4. Rune 39 3.3.5. 字符串的 for range 循环 40 3.3.6. 用字节切片构造字符串 41 3.3.7. 用rune切片构造字符串 42 3.3.8. 字符串的长度 42 3.3.9. 字符串是不可变的 42 3.3.10. UTF8(go圣经) 43 3.4. 常量 45 3.4.1. 常量定义 45 3.4.2. 常量类型 46 3.4.3. Iota 46 4. 组合数据类型 47 4.1. 数组 47 4.1.1. 数组概述 47 4.1.2. 数组的声明 49 4.1.3. 数组的长度 50 4.1.4. 遍历数组 50 4.1.5. 多维数组 51 4.2. 切片 52 4.2.1. 什么是切片 52 4.2.2. 切片概述 55 4.2.3. 创建一个切片 55 4.2.4. 切片遍历 57 4.2.5. 切片的修改 58 4.2.6. 切片的长度和容量 60 4.2.7. 追加切片元素 62 4.2.8. 切片的函数传递 65 4.2.9. 多维切片 66 4.2.10. 内存优化 67 4.2.11. nil slice和empty slice 69 4.2.12. For range 70 4.3. 结构 71 4.3.1. 什么是结构体? 71 4.3.2. 结构体声明 73 4.3.3. 结构体初始化 77 4.3.4. 嵌套结构体(Nested Structs) 81 4.3.5. 匿名字段 82 4.3.6. 导出结构体和字段 84 4.3.7. 结构体相等性(Structs Equality) 85 4.4. 指针类型 86 4.5. 函数 87 4.6. map 87 4.6.1. 什么是map 87 4.6.2. 声明、初始化和make 89 4.6.3. 给 map 添加元素 91 4.6.4. 获取 map 中的元素 91 4.6.5. 删除 map 中的元素 92 4.6.6. 获取 map 的长度 92 4.6.7. Map 的相等性 92 4.6.8. map的排序 92 4.7. 接口 93 4.7.1. 什么是接口? 93 4.7.2. 接口的声明与实现 96 4.7.3. 接口的实际用途 97 4.7.4. 接口的内部表示 99 4.7.5. 空接口 102 4.7.6. 类型断言 105 4.7.7. 类型选择(Type Switch) 109 4.7.8. 实现接口:指针接受者与接受者 112 4.7.9. 实现多个接口 114 4.7.10. 接口的嵌套 116 4.7.11. 接口的零 119 4.8. Channel 120 4.9. 类型转换 120 5. 函数 120 5.1. 函数的声明 121 5.2. 一个递归函数的例子( recursive functions) 121 5.3. 多返回 121 5.4. 命名返回 121 5.5. 可变函数参数 122 5.6. Defer 123 5.6.1. Defer语句介绍 123 5.6.2. Defer使用场景 128 5.7. 什么是头等(第一类)函数? 130 5.8. 匿名函数 130 5.9. 用户自定义的函数类型 132 5.10. 高阶函数(装饰器?) 133 5.10.1. 把函数作为参数,传递给其它函数 134 5.10.2. 在其它函数中返回函数 134 5.11. 闭包 135 5.12. 头等函数的实际用途 137 6. 微服务创建 140 6.1. 使用net/http创建简单的web server 140 6.2. 读写JSON 144 6.2.1. Marshal go结构到JSON 144 6.2.2. Unmarshalling JSON 到Go结构 146 7. 方法 146 7.1. 什么是方法? 146 7.2. 方法示例 146 7.3. 函数和方法区别 148 7.4. 指针接收器与接收器 153 7.5. 那么什么时候使用指针接收器,什么时候使用接收器? 155 7.6. 匿名字段的方法 156 7.7. 在方法中使用接收器 与 在函数中使用参数 157 7.8. 在方法中使用指针接收器 与 在函数中使用指针参数 159 7.9. 在非结构体上的方法 161 8. 并发入门 162 8.1. 并发是什么? 162 8.2. 并行是什么? 162 8.3. 从技术上看并发和并行 163 8.4. Go 对并发的支持 164 9. Go 协程 164 9.1. Go 协程是什么? 164 9.2. Go 协程相比于线程的优势 164 9.3. 如何启动一个 Go 协程? 165 9.4. 启动多个 Go 协程 167 10. 信道channel 169 10.1. 什么是信道? 169 10.2. 信道的声明 169 10.3. 通过信道进行发送和接收 169 10.4. 发送与接收默认是阻塞的 170 10.5. 信道的代码示例 170 10.6. 信道的另一个示例 173 10.7. 死锁 174 10.8. 单向信道 175 10.9. 关闭信道和使用 for range 遍历信道 176 11. 缓冲信道和工作池(Buffered Channels and Worker Pools) 179 11.1. 什么是缓冲信道? 179 11.2. 死锁 182 11.3. 长度 vs 容量 183 11.4. WaitGroup 184 11.5. 工作池的实现 186 12. Select 188 12.1. 什么是 select? 188 12.2. 示例 189 12.3. select 的应用 190 12.4. 默认情况 190 12.5. 死锁与默认情况 191 12.6. 随机选取 191 12.7. 这下我懂了:空 select 191 13. 文件读写 191 13.1. GoLang几种读文件方式的比较 197 14. 个人 197 14.1. ++,-- 198 14.2. 逗号 198 14.3. 未使用的变量 199 14.4. Effective go 199 14.4.1. 指针 vs. 199 14.5. 可寻址性-map和slice的区别 201 14.5.1. slice 201 14.5.2. map 202 14.6. golang库 203 14.6.1. unicode/utf8包 203 14.6.2. time包 205 14.6.3. Strings包 205 14.6.4. 输入输出 212 14.6.5. 正则处理 224 14.6.6. Golang内建函数 226

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值