GO 引用类型

本文借用 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
50
using 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
3
Agent.Id = 7

Mercenary.Id = 0

GO 语言只有 struct,也就是传递变量都是按值传递
那么每传递一次就会复制一份值,相当浪费内存以及产生分配内存回收内存等消耗性能的操作,为了能像按引用传递那样,可以用指针。
示例代码如下:

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
package main

import "fmt"

// 特工
type Agent struct {
ID int32
}

func main() {
agent := Agent{ID: 7}

doTask(agent) // 按值传递,相当于复制了一个特工
fmt.Printf("%v \n", agent.ID)

doMission(&agent) // 按引用传递
fmt.Printf("%v \n", agent.ID)
}

func doTask(agent Agent) {
agent.ID = 0 // 复制了,所以改变内部不会影响外部的
}

func doMission(agent *Agent) {
agent.ID = 0 // 指针指向同一个变量
}

运行结果如下:

1
2
7
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

// 特工
type Agent struct {
ID int32
}

// 影分身
func (agent Agent) ShadowClone() (clone Agent) {
return agent // 即使直接把传进来的返回,也相当于复制了
}

func (agent *Agent) AgentFun() {
}

func main() {
agent := Agent{ID: 7}
fmt.Printf("%p \n", &agent)

sClone := agent.ShadowClone()
fmt.Printf("%p \n", &sClone) //指针地址不一样,说明是不同的对象
}

运行结果如下:

1
2
0xc00004e080
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
41
package 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
2
3
4
5
6
7
8
9
10
%!p(int=0)  // 输出 !p 说明不是指针
%!p(bool=false)
%!p(main.Agent={0})
%!p([2]int=[0 0])

-- 华丽分割线 --

0x0 // 是一个指针,只是没有值而已,
0x0
0x0

从官方函数 fmt.Printf 中可以看出 slice,map,channel 在 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
41
func main() {
// 指针
var p uintptr

var m map[int]string
var s []int
var c chan int

fmt.Println("---'零'值的情况下---")
fmt.Println("Sizeof(p): ", unsafe.Sizeof(p)) // Sizeof(p): 8
fmt.Println("Sizeof(m): ", unsafe.Sizeof(m)) // Sizeof(m): 8
fmt.Println("Sizeof(s): ", unsafe.Sizeof(s)) // Sizeof(s): 24
fmt.Println("Sizeof(c): ", unsafe.Sizeof(c)) // Sizeof(c): 8

m = make(map[int]string)
m[1] = "a"
m[2] = "b"
m[3] = "c"
m[4] = "e"
m[5] = "f"

s = make([]int, 0)
s = append(s, 1)
s = append(s, 2)
s = append(s, 3)
s = append(s, 4)
s = append(s, 5)

c = make(chan int, 10)
c <- 1
c <- 2
c <- 3
c <- 4
c <- 5

fmt.Println("---赋值的情况下---")
fmt.Println("Sizeof(p): ", unsafe.Sizeof(p)) // Sizeof(p): 8
fmt.Println("Sizeof(m): ", unsafe.Sizeof(m)) // Sizeof(m): 8
fmt.Println("Sizeof(s): ", unsafe.Sizeof(s)) // Sizeof(s): 24
fmt.Println("Sizeof(c): ", unsafe.Sizeof(c)) // Sizeof(c): 8
}

首先分析一下 slice 的大小为什么是 24
Go 源码:https://github.com/golang/go/blob/master/src/runtime/slice.go

1
2
3
4
5
6
7
8
9
type slice struct {
array unsafe.Pointer // 64位系统,指针 8 个 byte
len int // 64位系统,int是 8 个 byte
cap int // 64位系统,int是 8 个 byte
} // 所以 slice 的大小是 8*3=24

func makeslice(et *_type, len, cap int) unsafe.Pointer {
// ... unsafe.Pointer 指向?
}

接着分析 map 的大小为什么是 8
Go 源码:https://github.com/golang/go/blob/master/src/runtime/map.go

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
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
// ... 可以看到返回的是一个指针地址
}

// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed

buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)

extra *mapextra // optional fields
}

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 本身就是传递指针的值,所以不需要用指针形式传递。

觉得文章对您有帮助,请我喝瓶肥宅快乐水可好 (๑•̀ㅂ•́)و✧
  • 本文作者: 阿彬~
  • 本文链接: https://iweixubin.github.io/posts/go/reference-types/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 免责声明:本媒体部分图片,版权归原作者所有。因条件限制,无法找到来源和作者未进行标注。
         如果侵犯到您的权益,请与我联系删除