本文借用 C#语言 的值类型
和引用类型
概念来理解 GO语言 定义成员方法和函数传参需要注意的地方。
值类型 与 引用类型
在编程的时候,需要知道构造声明出来变量的类型是值类型
还是引用类型
,
因为只有这样,你才会清楚地知道构造出来的数据什么时候会被回收,以及在函数(方法)传递过程是按值传递
还是按引用传递
。
请看下面的 C# 代码和注释:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50using System;
namespace ConsoleApp
{
// 特工
struct Agent // struct 是值类型
{
public int Id { get; set; }
}
// 雇佣兵
class Mercenary // class 是引用类型
{
public int Id { get; set; }
}
class Program
{
static void Main(string[] args)
{
// 实例化一个 struct,它将创建在 栈 上,超出变量作用域,出栈。
Agent agent = new Agent() { Id = 7 };
DoTask(agent);
// 实例化一个 class,它将创建在 堆 上,由 GC 负责回收
Mercenary mercenary = new Mercenary() { Id = 7 };
DoMission(mercenary);
Console.WriteLine($"Agent.Id = {agent.Id}");
Console.WriteLine();
Console.WriteLine($"Mercenary.Id = {mercenary.Id}");
Console.Read();
}
// 值类型 传递给方法 是传递 值,也就相当于 复制
static void DoTask(Agent agent) // 希望路过的 C# 程序猿还要去了解 ref 关键字
{
// 因为是复制了一个新的 agent,
agent.Id = 0; // 所以方法内修改并不会影响到外面
}
// 引用类型 传递给方法 是传递 引用,
static void DoMission(Mercenary mercenary)
{
// 因为是引用是指向同一个对象,
mercenary.Id = 0; // 所以方法内修改会影响到外面原来的变量
}
}
}
运行结果如下:1
2
3Agent.Id = 7
Mercenary.Id = 0
GO 语言只有 struct,也就是传递变量都是按值传递
,
那么每传递一次就会复制一份值,相当浪费内存以及产生分配内存和回收内存等消耗性能的操作,为了能像按引用传递
那样,可以用指针。
示例代码如下:
1 | package main |
运行结果如下:
1 | 7 |
1 | package main |
运行结果如下:1
20xc00004e080
0xc00004e0a0
因此 GO语言 一般传递 struct 变量都是用指针的形式传递,
但在 GO语言 中有些特殊的类型,不需要用指针的形式传递,如:slice,map,channel。
请看一下示例代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41package main
import "fmt"
// 特工
type Agent struct {
ID int32
}
func main() {
// "零"值是 0
var i int
fmt.Printf("%p \n", i)
// "零"值是 false
var b bool
fmt.Printf("%p \n", b)
// "零"值是 各成员都是 "零"值
var agent Agent
fmt.Printf("%p \n", agent)
// "零"值是 各成员都是 "零"值
var arrays [2]int
fmt.Printf("%p \n", arrays)
fmt.Println("\n -- 华丽分割线 -- \n")
// "零"值是 nil
var slice []int
fmt.Printf("%p \n", slice)
// "零"值是 nil
var dic map[string]int
fmt.Printf("%p \n", dic)
// "零"值是 nil
var ch chan int
fmt.Printf("%p \n", ch)
}
运行结果如下:
1 | %!p(int=0) // 输出 !p 说明不是指针 |
从官方函数 fmt.Printf
中可以看出 slice,map,channel 在 Go 语言设计者眼中是特殊对待的。
以下提供一段代码体验 slice,map,channel 的特殊性:
1 | func main() { |
首先分析一下 slice 的大小为什么是 24
Go 源码:https://github.com/golang/go/blob/master/src/runtime/slice.go
1 | type slice struct { |
接着分析 map 的大小为什么是 8
Go 源码:https://github.com/golang/go/blob/master/src/runtime/map.go
1 | // makemap implements Go map creation for make(map[k]v, hint). |
chan 和 map 类似:
Go 源码:https://github.com/golang/go/blob/master/src/runtime/chan.go
Go 没有引用传递概念
Go 有引用类型的概念,《Go 语言圣经》 12.2节是将 Go 的类型分为:
- 基础类型
- int
- bool
- …
- 聚合类型
- 数组
- struct
- 引用类型
- map
- slices
- pointers
- channels
- functions
Go虽然有引用类型的概念,且没有按引用传递
的概念,
正如文章一开始就说了,按引用传递
是 C# 语言的概念,其实 GO 语言一切都是按值传递
的。
因为传递指针本质上还是复制了一个指针地址,一个指针的大小就相当于int64的长度,消耗得起。
而方法内知道变量的所在内存地址,就可以操作该变量了,而不用完完全全复制一个新的变量来操作。
总结
那么在定义 Go 函数的时候,什么时候用变量值形式,什么时候用指针形式呢?
基础类型: string、bool、int 等一般用变量值形式,当然出于某种目的可以指针形式,比如累计计数,或者 string 太大。
聚合类型: struct 一般传递指针(用指针形式传递),当然出于某种目的可以变量值形式,比如要克隆复制。
引用类型: slice、map、chan 本身就是传递指针的值,所以不需要用指针形式传递。