「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。
帧边界(Frame Boundaries)
函数运行在帧边界的范围内,这个范围内为每个独立的函数提供了单独的内存空间。每一个帧提供给该函数上下文以及允许该函数的流程控制。函数通过指针可以直接访问帧范围内的内存。但是一个函数如果要访问帧外的内存,这部分内存必须共享给该函数,函数才可以间接访问该内存。
当一个函数被调用时,两个帧之间会发生转换,从调用者所在帧转移到被调用者所在帧。如果被调用者需要一部分调用者的数据,这部分数据需要传入被调用者所在的帧。在Go中,这种帧之间传递的数据是按值传递。这种方式的好处是所见及所得,即要传入的数据是可以直接看到的。
下面这个程序执行了一个函数调用,“按值”传递整数数据:
01 package main
02
03 func main() {
04
05 // Declare variable of type int with a value of 10.
06 count := 10
07
08 // Display the "value of" and "address of" count.
09 println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
10
11 // Pass the "value of" the count.
12 increment(count)
13
14 println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
15 }
16
17 //go:noinline
18 func increment(inc int) {
19
20 // Increment the "value of" inc.
21 inc++
22 println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
23 }
当一个go程序开始运行,runtime会创建一个主goroutine并开始执行所有的初始化代码以及main函数中的代码,如下图所示
该goroutine由一个操作系统线程运行,该线程又由某个cpu运行,如下图所示对应关系,在golang中,一个操作系统线程对应多个协程。
如下图所示,栈中一部分是Main函数的栈帧,其中放置了count变量及其地址。Main Frame是当前活跃的栈帧。
地址(addresses)
变量(varibales)是为某一个内存位置提供了一个名字,有利于阅读和编写代码。如果有一个变量,必然会有值在内存中,内存中有值则必然有一个内存地址。如下所示代码,count是变量,valueof是count的值,&count则是count的地址。对应关系是variable(可读性) → variable value→ variable address
09 println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
函数调用(functions call)
调用函数则意味着要创建新的栈帧,如下所示的函数,传递了count的值。这个值会被复制,传递并且放置在新的increment函数对应的栈帧中,increment函数只能直接读写在它栈帧中的内存。increment复制了count并存储在了inc变量中,在其内部调用inc。
12 increment(count)
该goroutine对应的栈现在如下图所示,Main对应一个frame,increment对应向下增长的新的Frame,可以看到inc变量的地址比count变量在更低的地址上,因为栈是向下增长。(不必纠结于实现细节)
increment中21,22行代码执行后,控制(control)权返还给了main函数。
21 inc++
22 println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
函数返回(Function Returns)
当函数返回以及控制返还给了调用者时,栈上的内存发生了什么?
简短的答案是什么都没有。下图是increment函数返回后的栈图,可以看到唯一的变化就是increment函数对应的栈帧被划归到了无效内存中。清理栈中的内存会浪费时间,因为这一块内存不知道什么时候会被用到。
共享值(sharing values)
如果对于increment而言,直接操纵main函数对应栈帧中的count变量很重要,指针可以让函数读写不同栈帧上的值。阅读源码时,看到&符号,可以将其替换为脑中的“共享”一词,因为指针就是用来共享的。如果没有在脑海里出现”共享“这一次,则意味着我们不需要用指针。
指针类型(Pointer Types)
对于每种被声明的类型(type),都会有一个对应的指针类型。对于built-in 里的类型,如int对应的指针类型就是int。如果自己声明了一个类型,如User则会对应一个User的指针类型(Pointer type)
所有的指针类型都会有两个相同的特征:
1、都起始于一个符号 *
2、都拥有同样的内存大小(memory size)和表示法(representation),4或8个字节(byte)表示地址
tip:(On 32bit architectures (like the playground), pointers require 4 bytes of memory and on 64bit architectures (like your machine), they require 8 bytes of memory.)
间接内存访问
对increment函数传递的参数做一些改变,如下代码所示
12 increment(&count)
这一次改变,将increment传递的内存变为了地址值,意味着“共享”了count变量。这仍然是值传递,因为这次传递的是mainframe中 count的地址值。指针不能接受所有的地址值,除非这个地址值与指针类型关联。(思考:传递指针值和基本类型占用的内存区别不会太大,但是传递结构体指针仅仅只需要4或8字节,结构体本身要比4或8字节大多了)
执行了该函数后的栈图如下所示:
21 *inc++
21行是修改后的increment内部,使用*作为操作符意味着“指针指向的值”,有时这种间接的读写被称为指针解引用。自增函数其帧内必须有一个指针变量,可以间接的去读取和写入另一个帧内的变量。
执行后的栈图如下所示,可以看到count被修改了。
总结
这篇文章翻译了参考文章中的第一篇,以及带入了一些自己对于指针传递以及go程序启动的思考。