首页 > 其他分享 >Go 语言机制之栈与指针

Go 语言机制之栈与指针

时间:2022-11-11 22:37:45浏览次数:43  
标签:count 函数 之栈 increment Go inc 变量 指针


原文作者:William Kennedy

四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!

原文如下:

序言

这个系列包含四篇文章,主要讲解 Go 言语指针、栈、堆、逃逸分析和值/指针语义背后的机制和设计理念。这是系列第一篇文章,主要讲解栈和指针。

介绍

我并不打算为指针说好话,它确实很难理解。如果使用不当,会导致惹人厌的 bug,甚至是性能问题。在编写并发或多线程软件时尤其如此。这也难怪很多编程语言都试图为程序员规避使用指针。然而,如果使用 Go 语言编程程序,指针是无法避免的。只有深入理解指针,你才能够写出干净、简洁且有效率的代码。

帧边界

帧边界为每个函数提供了单独的内存空间,函数就在帧边界范围内执行。帧边界允许函数在自己的上下文中运行,还提供流程控制。函数可以通过帧指针直接访问帧内的内存,而访问帧外内存只能通过间接的方式。对于每个函数来说,若想能够访问到帧外的内存,这块内存必须与函数共享。要想知道共享实现的,我们需要先学习和理解帧边界建立的机制和限制条件。

当一个函数被调用时,两个帧边界之间会发生上下文切换。从调用函数到被调用函数,如果函数调用时需要传递参数,这些参数也必须传递要被调函数的帧边界之内。Go 语言里面,两个帧之间的数据传递是按值传递的。

按值传递数据的优点是可读性好。在函数调用时,你看到的值就是在函数调用者和被调用者之间被复制和接收的值。这就是为什么我把“按值传递”与所见即所得联系在一起,因为你看到的就是你得到的。

让我们来看一段按值传递整型数据的代码:

清单1

package main

func main() {

// Declare variable of type int with a value of 10.
count := 10

// Display the "value of" and "address of" count.
println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")

// Pass the "value of" the count.
increment(count)

println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
}

//go:noinline
func increment(inc int) {

// Increment the "value of" inc.
inc++
println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
}

当你启动 Go 程序时,运行时将会创建主协程去执行所有的初始化代码包括 main() 函数里面的代码。goroutine 是一个放置在操作系统线程上面的执行路径,最终在某一个内核上执行。从 Go 1.8 版本中,每一个 goroutine 将会分配 2048 字节的连续内存块作为它的栈空间。几年以来,初始栈空间的大小一直在变化,以后还可能再次改变。

栈非常重要,因为它为每个单独函数的帧边界提供了物理内存空间。按照清单 1 ,当主协程执行 main() 函数的时候,栈空间的分布如下图这样:

Go 语言机制之栈与指针_指针变量

图 1

你可以看到图一,主函数的栈的一部分已经被框出来了。这部分称为“栈帧”,这个帧表示主函数在栈上的边界。帧是被调用函数执行的时候建立的,你还可以看到,变量 count 被放置在 main() 函数帧里面、内存地址为 0x10429fa4 位置。

图一还说明了另外一个有趣的点,活动帧以下的所有栈内存是不可用的,只有活动帧及其以上的栈内存是可用的。可用栈空间和不可用栈空间之间的边界需要明确下。

地址

变量的目的就是给特定的内存地址分配一个名称,使代码的可读性更强并且帮助你分析正在处理的数据。如果你有一个变量就能得到它保存在内存的值,内存地址中肯定有一个地址保存这个值。第 9 行代码,main() 函数调用内置函数 println() 显示变量 count 的值和地址。

清单2

println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")

使用 & 运算符获取变量所在内存位置的地址并不奇怪,其他语言也使用这个运算符。如果你的代码运行在 32 位电脑上,例如:go playground,那么输出会类似于下面这样:

清单3

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

函数调用

接下来的第 12 行代码,main() 函数调用 increment() 函数。

清单4

increment(count)

调用函数意味着协程需要在栈上构建出一块新的栈帧。但是,事情有点复杂。要想成功地调用函数,在发生上下文切换时,数据需要跨越帧边界传递到新的帧范围内。具体一点来说,函数调用的时候,整型值会被复制和传递。通过第 18 行代码、increment() 函数的声明,你就可以知道。

清单5

func increment(inc int) {

如果你回过头来再次看第 12 行代码函数 increment() 的调用,你会发现 count 变量是传值的。这个值会被拷贝、传递,最后存储在 increment() 函数的栈中。记住,increment() 函数只能在自己的栈内读写内存,因此,它需要 inc 变量来接收、存储和访问传递的 count 变量的副本。

就在 increment() 函数内部代码开始执行之前,协程的栈(站在一个非常高的角度)应该是像下图这样的:

Go 语言机制之栈与指针_调用函数_02

图 2 

你可以看到栈上现在有两个帧,一个属于 main() 函数,另一个属于 increment() 函数。在 increment() 函数的帧里面,你可以看到 inc 变量,它的值 10,是函数调用时拷贝、传递进来的。变量 inc 的地址是 0x10429f98,因为栈帧是从上至下使用栈空间的,所以它的内存地址较小,这只是具体的实现细节,并没任何意义。重要的是,协程从 main() 的栈帧里获取变量 count 的值,并使用 inc 变量将该值的副本放置在 increment() 函数的栈帧里。

increment() 函数的剩余代码显示 inc 变量的值和地址。

清单6

inc++
println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")

第 22 行代码输出类似下面这样:

清单7

inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]

执行这些代码之后,栈就会像下面这样:

Go 语言机制之栈与指针_调用函数_03

图 3

第 21、22 行代码执行之后,increment() 函数返回并且 CPU 控制权交还给 main() 函数。第 14 行代码,main() 函数会再次显示 count 变量的值和地址。

清单8

println("count:\tValue Of[",count, "]\tAddr Of[", &count, "]")

上面例子完整的输出会像下面这样:

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]
inc: Value Of[ 11 ] Addr Of[ 0x10429f98 ]
count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ]

main() 函数栈帧里,变量 count 的值在调用 increment() 函数前后是相同的。

函数返回

当函数返回并且控制权交还给调用函数时,栈上的内存实际上会发生什么?回答是:不会发生任何事情。当 increment() 函数返回时,栈上的空间看起来像下面这样:

Go 语言机制之栈与指针_指针变量_04

图 4

除了为 increment() 函数创建的栈帧变得不可用之外,栈的分布与图 3 基本是一样的。这是因为 main() 函数的帧变成了活动帧。对 increment() 函数的栈帧不做任何处理。

函数返回时,清理函数的帧会浪费时间,因为你不知道还会不会再次使用这块内存。所以这块内存就不会做任何处理。每次函数调用的时候,当需要帧的时候,栈上开辟的帧会被清理。这是通过存储在该帧里的变量初始化时完成的。因为所有的值会初始化成对应的零值,每次函数调用时,栈都会正确地完成自我清理工作。

共享值

如果 increment() 函数直接操作存储在 main() 函数帧里面的 count 变量非常重要,那该怎么办?这就要用到指针!指针的目的就是实现在函数间共享值,即使这个值不在自己函数的帧里面,函数也能够对它进行读写。

如果脑海里没有共享的概念,你可能不会使用指针。学习指针时,重要的是使用清晰的词汇,而不是单纯地记住操作符或者语法。因此,请记住,指针是用于共享的并且在阅读代码时,提到“共享”时,就应该想到 & 操作符。

指针类型

不管是你自定义的或者是 Go 语言自带的,对于每一种已声明的类型,都可以基于这些类型获得对应的指针类型用于共享。例如内置类型 int,对应的指针类型是 *int。如果你自己声明了类型 User,对应的指针类型就是 *User。

所有的指针类型有相同的特点。首先,它们以 * 符号开头;其次,占用相同的内存空间并且都表示一个地址,使用 4 个或 8 个字节长度表示一个地址。在 32 位机器上(例如 playground ),指针需要 4 个字节的内存空间;在 64 位机器上(例如你的电脑),需要 8 个字节的内存空间。

规范里有说明,指针类型可以看成是类型字面量,这意味着它们是有现有类型组成的未命名类型。

间接访问内存

让我们来看一段代码,这段代码展示了函数调用时按值传递地址。main() 和 increment() 函数的栈帧会共享 count 变量:

清单10

package main

func main() {

// Declare variable of type int with a value of 10.
count := 10

// Display the "value of" and "address of" count.
println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")

// Pass the "address of" count.
increment(&count)

println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
}

//go:noinline
func increment(inc *int) {

// Increment the "value of" count that the "pointer points to". (dereferencing)
*inc++
println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]\tValue Points To[", *inc, "]")
}

基于原来的代码有三处改动的地方,第 12 行是第一处改动:

清单11

increment(&count)

现在,第 12 行代码拷贝、传递的并非 count 变量的值,而是变量的地址。可以认为,main() 函数与 increment() 函数是共享 count 变量的。这是 & 操作符起的作用。

重点理解,现在依旧是传值,唯一不同的是现在传递的是地址而不是一个整型数据。地址也是一个值,是函数调用时会跨帧边界发生拷贝和传递的内容。

因为地址会发生拷贝和传递,在 increment() 函数里面需要一个变量接收和存储该地址值。所以在第 18 行声明了整型的指针变量。

清单12

func increment(inc *int) {

如果你传递的是 User 类型值的地址,变量就应该声明成 *User。尽管指针变量存储的是地址,也不能传递任何类型的地址,只能传递与指针类型相一致的地址。关键在于,共享值的原因是因为接收函数能够对值进行读写操作。只有知道值的类型信息才能够进行读写操作。编译器会保证只有与指针类型相一致的值才能够实现函数间共享。

调用 increment() 函数时候,栈空间就像下面这样:

Go 语言机制之栈与指针_调用函数_05

图 5

当一个地址作为值执行按值传递之后,你可以从图 5 看出栈是如何分布的。现在,increment() 函数帧空间里面的指针变量指向 count 变量,该变量在 main() 函数的帧空间里。

通过使用指针变量,increment() 函数可以间接对 count 变量执行读写操作。

清单 13

*inc++

这一次,字符 * 充当操作符,与指针变量搭配使用。使用 * 操作符是“获取指针指向的值”的意思。指针变量允许在帧外对函数帧内的内存进行间接访问。有时候,间接的读写操作也称为解引用。increment() 函数必须有指针变量,才能够对其他函数帧空间执行间接访问。

执行第 21 行代码之后,栈空间分布如图 6 所示。

Go 语言机制之栈与指针_函数调用_06

图 6 

程序最后输出:

清单 14

count:  Value Of[ 10 ]              Addr Of[ 0x10429fa4 ]
inc: Value Of[ 0x10429fa4 ] Addr Of[ 0x10429f98 ] Value Points To[ 11 ]
count: Value Of[ 11 ] Addr Of[ 0x10429fa4 ]

你可以看到,指针变量 inc 的值和 count 变量的地址是相同的。这将建立起共享关系,允许在帧外执行内存的间接访问。在 increment() 函数里,一旦通过指针执行了写操作,改变也会体现在 main() 函数里。

指针变量并不特别

指针变量并不特别,它们和其他变量一样也是变量,有内存地址和值。正巧的是,无论指针变量指向的值的类型如何,所有的指针变量都有同样的大小和表现形式。唯一困惑的是使用 * 字符充当操作符,用来声明指针类型。如果你能分清指针类型声明和指针操作,你就没有那么困惑了。

总结

这篇文章描述了设计指针背后的目的和 Go 语言中栈和指针的工作机制。这是理解 Go 语言机制、设计哲学的第一步,也对编写一致性且可读性的代码提供一些指导作用。

总结一下,通过这篇文章你能学习到的知识:

1.帧边界为每个函数提供了单独的内存空间,函数就在帧范围内执行;2.当函数调用时,上下文环境会在两个帧之间发生切换;3.按值传递的优点是可读性好;4.栈很重要,因为它为每个函数的帧边界提供了可访问的物理内存空间;5.活动帧以下的所有栈内存是不可用的,只有活动帧及其上方的栈内存是有用的;6.调用函数意味着协程会在栈内存上开辟一块新的栈帧;7.每次函数调用的时候,当使用到帧时,相应的栈内存会被初始化;8.设计指针的目的是实现函数间值共享,即使该值不在函数自己栈帧里,也能对其进行读写操作;9.对于每一种类型,不管是自己定义的还是 Go 语言内置的,都有相应的指针类型;10.通过使用指针变量,允许在函数帧外进行间接内存访问;11.与其他变量相比,指针变量并没有特别之处,因为它们也是变量,有内存地址和值。


Go 语言机制之栈与指针_调用函数_07

Go 语言机制之栈与指针_函数调用_08


标签:count,函数,之栈,increment,Go,inc,变量,指针
From: https://blog.51cto.com/u_15289640/5845472

相关文章

  • 杂谈 | 在 macOS 上使用 Hugo + Coding 搭建个人博客
    文章目录​​前言​​​​旅途特色​​​​QuickStart​​​​一、Hugo配置以及使用​​​​1.Hugo下载安装​​​​2.创建本地网站​​​​3.下载喜欢的HugoTheme......
  • 指针(待续……)
    int*p;定义了指针变量p,这个变量储存了一个地址,这个地址对应的变量是int类型的。&是取地址符,int*p=&x;就定义了一个指向x的指针。指向相同类型变量的指针之间可以相互赋......
  • 部署ArgoCD
    官网https://github.com/argoproj/argo-cd/releases/tag/v2.5.2我部署的事Non-HA版本kubectlcreatenamespaceargocdkubectlapply-nargocd-fhttps://raw.github......
  • django信号
    一、简介Django包含一个“信号调度器”,当框架中其他地方发生动作时,它可以帮助解耦的应用程序得到通知。简而言之,信号允许某些发送者通知一组接收者某些动作已经发生。信......
  • python调用golang 从指定序列中找出一组与目标值最接近的子序列 kayb
    python调用golang从指定序列中找出一组与目标值最接近的子序列编写go代码生成so库python代码调用编写go代码写入hello.go文件packagemainimport( "C" "en......
  • go打包成rpm
    # 1.环境准备首先是rpmbuild的相关依赖,无论是什么语言写的程序,只要是打包成rpm包,就都需要这些yum install -y gcc make rpm-build redhat-rpm-configrpmbui......
  • Go_day05
    Go基础语法OOP面向对象Go语言本身不是面向对象的语言,但是可以通过一些方法来模拟对象面向对象的思维就是分类思维继承//定义一个父类结构体typePersonstruct{......
  • Gogs
    Gogs是一款极易搭建的自助Git服务。项目愿景Gogs(/gɑgz/)项目旨在打造一个以最简便的方式搭建简单、稳定和可扩展的自助Git服务。使用Go语言开发使得Gogs能够通......
  • 《Go 语言并发之道》读书笔记(一)
    已经把《Go语言并发之道》通读了一遍,非常不错的一本书,对于理解掌握Go语言的并发知识有很大的帮助,接下来我会把书中有用的知识通过代码示例出来,把一些比较好的知识点记录下......
  • mongoDB大数据查询遇到的问题
    有一个30亿量级数据的库,如何全量爬取并分析?因为量级过大无法一次性爬取至本地再分析,考虑使用limit().skip()混合的方法,一次读取1万条数据进行分析存储,30亿数据分成30万份后......