[译] Go Frequently Asked Questions(FAQ) - Pointers and Allocation | golang官方文档中文翻译之指针和内存分配

前言

本篇译文对应的原文
标题:Pointers and Allocation - Go Frequently Asked Questions(FAQ)
作者:Go官方文档
地址:https://golang.org/doc/faq#Pointers

本文标明yoko备注的内容是我自己写的备注,其余的都是对英文原文的翻译。

目录

  • 函数参数什么时候按值传递?
  • 方法定义在值类型上还是指针类型上?
  • new和make有什么区别?
  • int类型在64位系统上大小是多少?
  • 我如何知道一个变量是分配在堆上还是栈上?
  • 为什么我的golang进程占用了很多虚拟内存?
  • TODO
    • 什么时候需要使用指向interface的指针?

函数参数什么时候按值传递?

和所有的c语言家族的语言一样,golang中的所有东西都是按值传递。也就是说,函数总是获取到所传递实参的拷贝,就像有赋值语句把值传递给参数。比如说,传递一个int值给一个函数会拷贝这个int值,传递一个指针值也给拷贝这个指针,但不是拷贝指针所指向的数据。

map和slice类型的行为和指针类似:它们是包含了指向底层map和slice数据的描述符。拷贝map或slice时并不会拷贝它指向的数据。拷贝一个interface会拷贝interface存储的数据。如果interface存储的是一个结构体,拷贝这个interface会拷贝这个结构体。如果interface存储的是一个指针。拷贝这个interface会拷贝这个指针,但是同样的,不会拷贝它指向的数据。

方法定义在值类型上还是指针类型上?

1
2
func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct) valueMethod() { } // method on value

对于不熟悉指针的开发者,对这两个例子的区别可能有些困惑,但是实际情况其实十分简单。当在一个类型上定义一个方法时,接收者(上面例子中的s)的行为完全和将这个接收者作为这个方法的参数是一样的。考虑把接收者定义为值类型或指针类型,和把函数的参数定义为值类型或指针类型是一样的。有以下几点需要考虑。

首先,也是最重要的,方法是否需要修改接收者?如果需要,那么接收者必须为指针类型(slice和map表现得和引用一样,所以它们有些特殊,但是比如说在方法中改变slice的长度仍然需要指针类型)。在上面的例子,如果pointerMethod改变了s中元素的值,调用者会看到这变化,但是valueMethod是被调用者参数的一份拷贝所调用的(这就是按值传递的定义),所以改变s中元素的值对调用者是不可见的。

随便说一句,java中方法的接收者总是使用指针类型,虽然它们的指针本质上有些伪装(有一个提案是往java中添加值类型接收者)。golang中的值类型接收者是少见的。

第二点是考虑效率。如果接收者比较大,比如说一个大结构体,使用指针类型接收者代价会更小些。

接下来考虑一致性。如果有的方法的接收者类型必须为指针类型,那么其余方法也应该使用指针类型,使得方法集合是一致的,无论实际使用的调用类型是值类型还是指针类型。

对于基础类型,slice,小结构体这些类型,值类型接收者的代价是很低的,所以除非方法从语义上需要指针类型接收者,那么使用值类型接收者是高效且清晰的。

1
2
3
4
5
6
7
8
9
10
11
12
yoko备注
英文原文在说明一致性,方法集合时,引用FAQ中另外一个章节的内容:
https://golang.org/doc/faq#different_method_sets
我个人的理解是,
当方法定义为值类型接收者时,那么只能使用值类型调用
当方法定义为指针类型接收者时,那么不但能使用指针类型调用,还能使用值类型调用

如果已经有方法定义为指针类型接收者了,那么为了保持一致,其余的方法应该都定义为指针类型接收者
不然会造成指针类型的对象有的方法能调,有的不行

并且指针类型可以在方法内解引用得到实体对象
但是值类型由于要保持方法内修改对象对外不可见,那么在方法内获取对象的地址是不安全的

new和make有什么区别?

简单来说:new分配内存,而make用来初始化slice,map,channel这三个类型。

更详细的细节参见:https://golang.org/doc/effective_go.html#allocation_new

int类型在64位系统上大小是多少?

int和uint的大小是具体实现相关的,但是在一个指定的平台上它们是相等。为了可移植性,依赖特定大小的值的代码应该使用显式大小的类型,比如int64。在32位系统编译器默认使用32位整型,在64位系统整型使用64位(从历史来看,也不总是这样)。

另一方面,浮点类型总是特定大小固定的(没有浮点基础类型),因为开发者在使用浮点类型的数值时应该知道精度。默认类型用来表示应该一个未指定类型的浮点常量是float64。所以foo := 3.0声明了一个类型为float64的变量。对于一个float32类型的变量如果使用一个未指定类型的常量来初始化的话,变量的类型必须在变量声明时显式指定:

1
var foo float32 = 3.0

或者,常量必须使用类型转换就像这样 foo := float32(3.0)

我如何知道一个变量是分配在堆上还是栈上?

从正确性角度来说,你不需要知道一个变量是分配在堆上还是栈上。golang中的每个变量,只要还有引用,生命周期就会一直持续。golang选择变量存储在何位置的具体实现是不影响golang的语言语义的。

变量存储位置对编写高效程序是有影响的。如果可能的话,golang编译器会将函数的局部变量分配在函数的栈空间上。然后,如果编译器不能证明这个变量在函数返回后不会被引用,那么编译器必须将这个变量分配在带垃圾回收机制的堆上以避免出现野指针错误。并且,如果一个局部变量特别大,把它分配在堆上可能会比分配在栈上更好些。

在当前的编译器中,如果一个变量的地址被获取,那么这个变量可能会分配在堆上。然而,一个基础的逃逸分析可以识别出一些变量在函数返回后不再存活的场景,那么这种变量会继续保存在栈上。

为什么我的golang进程占用了很多虚拟内存?

golang的内存分配器预申请了一大块虚拟内存区域用于后续内存申请。这个虚拟内存是和具体的golang进程关联的;这个预分配行为不会剥夺其它进程的内存。

如果想知道一个golang进程的实际内存申请大小,使用Unix的top命令并观察RES(linux平台)或者RSIZE(macos平台)。

TODO

什么时候需要使用指向interface的指针?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yoko备注
什么时候需要使用指向interface的指针?

这一小节翻译的不好,建议阅读英文原文。
对本小节的临时翻译我也贴在本文末尾处了。

针对这个问题的答案是,不要使用指向interface的指针。

个人感觉它所表达的内容有两点:
第一点是,一个实际类型只要实现了一个interface的接口,
那么这个实际类型的值或者这个实际类型的指针都能够被这个interface接收。

第二点是,不要使用指向interface的指针(注意,并不是第一点中说的指向实际类型的指针,要区分开),
interface并不能接收指向该interface的指针,编译会报错。

几乎永远不要使用。指向interface的指针只出现在罕见的情况,它掩盖了interface值的类型用于延时取值。

一个常见的错误是传递一个指向interface值的指针给一个需要interface参数的函数。编译器会对此报错,但是这种情况仍然让人困惑,因为有时候指针需要满足interface。尽管一个指向具体类型的指针可以满足interface,但是有一种例外是一个指向interface的指针无法满足一个interface。

考虑以下变量声明

1
var w io.Writer

打印函数fmt.Fprintf把满足io.Writer接口的值(也就是实现了Write接口方法)作为第一个参数。所以我们可以像下面这样写

1
fmt.Fprintf(w, "hello, world\n")

然而如果我们传递w的地址,程序将编译失败。

1
fmt.Fprintf(&w, "hello, world\n") // Compile-time error.

一种例外情况是,任何值,即使是一个指向interface的指针,可以被赋值为空interface类型(interface{})。即使如此,指向interface的指针几乎肯定是一种错误;结果会令人困惑。

本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/62279/

本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/62279/

0%