大橙子网站建设,新征程启航
为企业提供网站建设、域名注册、服务器等服务
GO中的defer会在当前函数返回前执行传入的函数,常用于关闭文件描述符,关闭链接及解锁等操作。
专注于为中小企业提供成都网站设计、成都做网站、外贸网站建设服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业河北免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了上千家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。
Go语言中使用defer时会遇到两个常见问题:
接下来我们来详细处理这两个问题。
官方有段对defer的解释:
这里我们先来一道经典的面试题
你觉得这个会打印什么?
输出结果:
这里是遵循先入后出的原则,同时保留当前变量的值。
把这道题简化一下:
输出结果
上述代码输出似乎不符合预期,这个现象出现的原因是什么呢?经过分析,我们发现调用defer关键字会立即拷贝函数中引用的外部参数,所以fmt.Println(i)的这个i是在调用defer的时候就已经赋值了,所以会直接打印1。
想要解决这个问题也很简单,只需要向defer关键字传入匿名函数
这里把一些垃圾回收使用的字段忽略了。
中间代码生成阶段cmd/compile/internal/gc/ssa.go会处理程序中的defer,该函数会根据条件不同,使用三种机制来处理该关键字
开放编码、堆分配和栈分配是defer关键字的三种方法,而Go1.14加入的开放编码,使得关键字开销可以忽略不计。
call方法会为所有函数和方法调用生成中间代码,工作内容:
defer关键字在运行时会调用deferproc,这个函数实现在src/runtime/panic.go里,接受两个参数:参数的大小和闭包所在的地址。
编译器不仅将defer关键字转成deferproc函数,还会通过以下三种方式为所有调用defer的函数末尾插入deferreturn的函数调用
1、在cmd/compile/internal/gc/walk.go的walkstmt函数中,在遇到ODEFFER节点时会执行Curfn.Func.SetHasDefer(true),设置当前函数的hasdefer属性
2、在ssa.go的buildssa会执行s.hasdefer = fn.Func.HasDefer()更新hasdefer
3、在exit中会根据hasdefer在函数返回前插入deferreturn的函数调用
runtime.deferproc为defer创建了一个runtime._defer结构体、设置它的函数指针fn、程序计数器pc和栈指针sp并将相关参数拷贝到相邻的内存空间中
最后调用的return0是唯一一个不会触发延迟调用的函数,可以避免deferreturn的递归调用。
newdefer的分配方式是从pool缓存池中获取:
这三种方式取到的结构体_defer,都会被添加到链表的队头,这也是为什么defer按照后进先出的顺序执行。
deferreturn就是从链表的队头取出并调用jmpdefer传入需要执行的函数和参数。
该函数只有在所有延迟函数都执行后才会返回。
如果我们能够将部分结构体分配到栈上就可以节约内存分配带来的额外开销。
在call函数中有在栈上分配
在运行期间deferprocStack只需要设置一些未在编译期间初始化的字段,就可以将栈上的_defer追加到函数的链表上。
除了分配的位置和堆的不同,其他的大致相同。
Go语言在1.14中通过开放编码实现defer关键字,使用代码内联优化defer关键的额外开销并引入函数数据funcdata管理panic的调用,该优化可以将 defer 的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右。
然而开放编码作为一种优化 defer 关键字的方法,它不是在所有的场景下都会开启的,开放编码只会在满足以下的条件时启用:
如果函数中defer关键字的数量多于8个或者defer处于循环中,那么就会禁用开放编码优化。
可以看到这里,判断编译参数不用-N,返回语句的数量和defer数量的乘积小于15,会启用开放编码优化。
延迟比特deferBitsTemp和延迟记录是使用开放编码实现defer的两个最重要的结构,一旦使用开放编码,buildssa会在栈上初始化大小为8个比特的deferBits
延迟比特中的每一个比特位都表示该位对应的defer关键字是否需要被执行。延迟比特的作用就是标记哪些defer关键字在函数中被执行,这样就能在函数返回时根据对应的deferBits确定要执行的函数。
而deferBits的大小为8比特,所以该优化的条件就是defer的数量小于8.
而执行延迟调用的时候仍在deferreturn
这里做了特殊的优化,在runOpenDeferFrame执行开放编码延迟函数
1、从结构体_defer读取deferBits,执行函数等信息
2、在循环中依次读取执行函数的地址和参数信息,并通过deferBits判断是否要执行
3、调用reflectcallSave执行函数
1、新加入的defer放入队头,执行defer时是从队头取函数调用,所以是后进先出
2、通过判断defer关键字、return数量来判断是否开启开放编码优化
3、调用deferproc函数创建新的延迟调用函数时,会立即拷贝函数的参数,函数的参数不会等到真正执行时计算
按值传递函数参数,是拷贝参数的实际值到函数的形式参数的方法调用。在这种情况下,参数在函数内变化对参数不会有影响。
默认情况下,Go编程语言使用调用通过值的方法来传递参数。在一般情况下,这意味着,在函数内码不能改变用来调用所述函数的参数。考虑函数swap()的定义如下。
代码如下:
/* function definition to swap the values */
func swap(int x, int y) int {
var temp int
temp = x /* save the value of x */
x = y /* put y into x */
y = temp /* put temp into y */
return temp;
}
现在,让我们通过使实际值作为在以下示例调用函数swap():
代码如下:
package main
import "fmt"
func main() {
/* local variable definition */
var a int = 100
var b int = 200
fmt.Printf("Before swap, value of a : %d\n", a )
fmt.Printf("Before swap, value of b : %d\n", b )
/* calling a function to swap the values */
swap(a, b)
fmt.Printf("After swap, value of a : %d\n", a )
fmt.Printf("After swap, value of b : %d\n", b )
}
func swap(x, y int) int {
var temp int
temp = x /* save the value of x */
x = y /* put y into x */
y = temp /* put temp into y */
return temp;
}
让我们把上面的代码放在一个C文件,编译并执行它,它会产生以下结果:
Before swap, value of a :100
Before swap, value of b :200
After swap, value of a :100
After swap, value of b :200
这表明,参数值没有被改变,虽然它们已经在函数内部改变。
通过传递函数参数,即是拷贝参数的地址到形式参数的参考方法调用。在函数内部,地址是访问调用中使用的实际参数。这意味着,对参数的更改会影响传递的参数。
要通过引用传递的值,参数的指针被传递给函数就像任何其他的值。所以,相应的,需要声明函数的参数为指针类型如下面的函数swap(),它的交换两个整型变量的值指向它的参数。
代码如下:
/* function definition to swap the values */
func swap(x *int, y *int) {
var temp int
temp = *x /* save the value at address x */
*x = *y /* put y into x */
*y = temp /* put temp into y */
}
现在,让我们调用函数swap()通过引用作为在下面的示例中传递数值:
代码如下:
package main
import "fmt"
func main() {
/* local variable definition */
var a int = 100
var b int= 200
fmt.Printf("Before swap, value of a : %d\n", a )
fmt.Printf("Before swap, value of b : %d\n", b )
/* calling a function to swap the values.
* a indicates pointer to a ie. address of variable a and
* b indicates pointer to b ie. address of variable b.
*/
swap(a, b)
fmt.Printf("After swap, value of a : %d\n", a )
fmt.Printf("After swap, value of b : %d\n", b )
}
func swap(x *int, y *int) {
var temp int
temp = *x /* save the value at address x */
*x = *y /* put y into x */
*y = temp /* put temp into y */
}
让我们把上面的代码放在一个C文件,编译并执行它,它会产生以下结果:
Before swap, value of a :100
Before swap, value of b :200
After swap, value of a :200
After swap, value of b :100
这表明变化的功能以及不同于通过值调用的外部体现的改变不能反映函数之外。
切换到新语言始终是一大步,尤其是当您的团队成员只有一个时有该语言的先前经验。现在,Stream 的主要编程语言从 Python 切换到了 Go。这篇文章将解释stream决定放弃 Python 并转向 Go 的一些原因。
Go 非常快。性能类似于 Java 或 C++。对于用例,Go 通常比 Python 快 40 倍。
对于许多应用程序来说,编程语言只是应用程序和数据库之间的粘合剂。语言本身的性能通常并不重要。然而,Stream 是一个API 提供商,为 700 家公司和超过 5 亿最终用户提供提要和聊天平台。多年来,我们一直在优化 Cassandra、PostgreSQL、Redis 等,但最终,您会达到所使用语言的极限。Python 是一门很棒的语言,但对于序列化/反序列化、排名和聚合等用例,它的性能相当缓慢。我们经常遇到性能问题,Cassandra 需要 1 毫秒来检索数据,而 Python 会花费接下来的 10 毫秒将其转换为对象。
看看我如何开始 Go 教程中的一小段 Go 代码。(这是一个很棒的教程,也是学习 Go 的一个很好的起点。)
如果您是 Go 新手,那么在阅读那个小代码片段时不会有太多让您感到惊讶的事情。它展示了多个赋值、数据结构、指针、格式和一个内置的 HTTP 库。当我第一次开始编程时,我一直喜欢使用 Python 更高级的功能。Python 允许您在编写代码时获得相当的创意。例如,您可以:
这些功能玩起来很有趣,但是,正如大多数程序员会同意的那样,在阅读别人的作品时,它们通常会使代码更难理解。Go 迫使你坚持基础。这使得阅读任何人的代码并立即了解发生了什么变得非常容易。 注意:当然,它实际上有多“容易”取决于您的用例。如果你想创建一个基本的 CRUD API,我仍然推荐 Django + DRF或 Rails。
作为一门语言,Go 试图让事情变得简单。它没有引入许多新概念。重点是创建一种非常快速且易于使用的简单语言。它唯一具有创新性的领域是 goroutine 和通道。(100% 正确CSP的概念始于 1977 年,所以这项创新更多是对旧思想的一种新方法。)Goroutines 是 Go 的轻量级线程方法,通道是 goroutines 之间通信的首选方式。Goroutines 的创建非常便宜,并且只需要几 KB 的额外内存。因为 Goroutine 非常轻量,所以有可能同时运行数百甚至数千个。您可以使用通道在 goroutine 之间进行通信。Go 运行时处理所有复杂性。goroutines 和基于通道的并发方法使得使用所有可用的 CPU 内核和处理并发 IO 变得非常容易——所有这些都不会使开发复杂化。与 Python/Java 相比,在 goroutine 上运行函数需要最少的样板代码。您只需在函数调用前加上关键字“go”:
Go 的并发方法很容易使用。与 Node 相比,这是一种有趣的方法,开发人员必须密切关注异步代码的处理方式。Go 中并发的另一个重要方面是竞争检测器。这样可以很容易地确定异步代码中是否存在任何竞争条件。
我们目前用 Go 编写的最大的微服务编译需要 4 秒。与以编译速度慢而闻名的 Java 和 C++ 等语言相比,Go 的快速编译时间是一项重大的生产力胜利。我喜欢在程序编译的时候摸鱼,但在我还记得代码应该做什么的同时完成事情会更好。
首先,让我们从显而易见的开始:与 C++ 和 Java 等旧语言相比,Go 开发人员的数量并不多。根据StackOverflow的数据, 38% 的开发人员知道 Java, 19.3% 的人知道 C++,只有 4.6% 的人知道 Go。GitHub 数据显示了类似的趋势:Go 比 Erlang、Scala 和 Elixir 等语言使用更广泛,但不如 Java 和 C++ 流行。幸运的是,Go 是一种非常简单易学的语言。它提供了您需要的基本功能,仅此而已。它引入的新概念是“延迟”声明和内置的并发管理与“goroutines”和通道。(对于纯粹主义者来说:Go 并不是第一种实现这些概念的语言,只是第一种使它们流行起来的语言。)任何加入团队的 Python、Elixir、C++、Scala 或 Java 开发人员都可以在一个月内在 Go 上发挥作用,因为它的简单性。与许多其他语言相比,我们发现组建 Go 开发人员团队更容易。如果您在博尔德和阿姆斯特丹等竞争激烈的生态系统中招聘人员,这是一项重要的优势。
对于我们这样规模的团队(约 20 人)来说,生态系统很重要。如果您必须重新发明每一个小功能,您根本无法为您的客户创造价值。Go 对我们使用的工具有很好的支持。实体库已经可用于 Redis、RabbitMQ、PostgreSQL、模板解析、任务调度、表达式解析和 RocksDB。与 Rust 或 Elixir 等其他较新的语言相比,Go 的生态系统是一个重大胜利。它当然不如 Java、Python 或 Node 之类的语言好,但它很可靠,而且对于许多基本需求,你会发现已经有高质量的包可用。
Gofmt 是一个很棒的命令行实用程序,内置在 Go 编译器中,用于格式化代码。就功能而言,它与 Python 的 autopep8 非常相似。我们大多数人并不真正喜欢争论制表符与空格。格式的一致性很重要,但实际的格式标准并不那么重要。Gofmt 通过使用一种正式的方式来格式化您的代码来避免所有这些讨论。
Go 对协议缓冲区和 gRPC 具有一流的支持。这两个工具非常适合构建需要通过 RPC 通信的微服务。您只需要编写一个清单,在其中定义可以进行的 RPC 调用以及它们采用的参数。然后从这个清单中自动生成服务器和客户端代码。生成的代码既快速又具有非常小的网络占用空间并且易于使用。从同一个清单中,您甚至可以为许多不同的语言生成客户端代码,例如 C++、Java、Python 和 Ruby。因此,内部流量不再有模棱两可的 REST 端点,您每次都必须编写几乎相同的客户端和服务器代码。.
Go 没有像 Rails 用于 Ruby、Django 用于 Python 或 Laravel 用于 PHP 那样的单一主导框架。这是 Go 社区内激烈争论的话题,因为许多人主张你不应该一开始就使用框架。我完全同意这对于某些用例是正确的。但是,如果有人想构建一个简单的 CRUD API,他们将更容易使用 Django/DJRF、Rails Laravel 或Phoenix。对于 Stream 的用例,我们更喜欢不使用框架。然而,对于许多希望提供简单 CRUD API 的新项目来说,缺乏主导框架将是一个严重的劣势。
Go 通过简单地从函数返回错误并期望调用代码来处理错误(或将其返回到调用堆栈)来处理错误。虽然这种方法有效,但很容易失去问题的范围,以确保您可以向用户提供有意义的错误。错误包通过允许您向错误添加上下文和堆栈跟踪来解决此问题。另一个问题是很容易忘记处理错误。像 errcheck 和 megacheck 这样的静态分析工具可以方便地避免犯这些错误。虽然这些变通办法效果很好,但感觉不太对劲。您希望该语言支持正确的错误处理。
Go 的包管理绝不是完美的。默认情况下,它无法指定特定版本的依赖项,也无法创建可重现的构建。Python、Node 和 Ruby 都有更好的包管理系统。但是,使用正确的工具,Go 的包管理工作得很好。您可以使用Dep来管理您的依赖项,以允许指定和固定版本。除此之外,我们还贡献了一个名为的开源工具VirtualGo,它可以更轻松地处理用 Go 编写的多个项目。
我们进行的一个有趣的实验是在 Python 中使用我们的排名提要功能并在 Go 中重写它。看看这个排名方法的例子:
Python 和 Go 代码都需要执行以下操作来支持这种排名方法:
开发 Python 版本的排名代码大约花了 3 天时间。这包括编写代码、单元测试和文档。接下来,我们花了大约 2 周的时间优化代码。其中一项优化是将分数表达式 (simple_gauss(time)*popularity) 转换为抽象语法树. 我们还实现了缓存逻辑,可以在未来的特定时间预先计算分数。相比之下,开发此代码的 Go 版本大约需要 4 天时间。性能不需要任何进一步的优化。因此,虽然 Python 的最初开发速度更快,但基于 Go 的版本最终需要我们团队的工作量大大减少。另外一个好处是,Go 代码的执行速度比我们高度优化的 Python 代码快大约 40 倍。现在,这只是我们通过切换到 Go 体验到的性能提升的一个示例。
与 Python 相比,我们系统的其他一些组件在 Go 中构建所需的时间要多得多。作为一个总体趋势,我们看到 开发 Go 代码需要更多的努力。但是,我们花更少的时间 优化 代码以提高性能。
我们评估的另一种语言是Elixir.。Elixir 建立在 Erlang 虚拟机之上。这是一种迷人的语言,我们之所以考虑它,是因为我们的一名团队成员在 Erlang 方面拥有丰富的经验。对于我们的用例,我们注意到 Go 的原始性能要好得多。Go 和 Elixir 都可以很好地服务数千个并发请求。但是,如果您查看单个请求的性能,Go 对于我们的用例来说要快得多。我们选择 Go 而不是 Elixir 的另一个原因是生态系统。对于我们需要的组件,Go 有更成熟的库,而在许多情况下,Elixir 库还没有准备好用于生产环境。培训/寻找开发人员使用 Elixir 也更加困难。这些原因使天平向 Go 倾斜。Elixir 的 Phoenix 框架看起来很棒,绝对值得一看。
Go 是一种非常高性能的语言,对并发有很好的支持。它几乎与 C++ 和 Java 等语言一样快。虽然与 Python 或 Ruby 相比,使用 Go 构建东西确实需要更多时间,但您将节省大量用于优化代码的时间。我们在Stream有一个小型开发团队,为超过 5 亿最终用户提供动力和聊天。Go 结合了 强大的生态系统 、新开发人员的 轻松入门、快速的性能 、对并发的 可靠支持和高效的编程环境 ,使其成为一个不错的选择。Stream 仍然在我们的仪表板、站点和机器学习中利用 Python 来提供个性化的订阅源. 我们不会很快与 Python 说再见,但今后所有性能密集型代码都将使用 Go 编写。我们新的聊天 API也完全用 Go 编写。
在以下这段代码中,我们操作一个文件,无论成功与否都需要关闭文件句柄。这里在三处不同的位置都调用了file.Close()方法,代码显得非常冗余。
我们利用延迟调用来优化代码。定义后的defer代码,会在return之前返回,让代码显得更加紧凑,且可读性变强,对上面的代码改造如下:
我们通过这个示例来看一下延迟调用与正常代码之间的执行顺序
先简单分析一下代码逻辑:
从输出中,我们可以观察到如下现象:
从这个实例中,我们很明显观察到,defer语句是在return之前执行
如果一个函数内定义了多个defer,则调用顺序为LIFO(后进先出)方式执行。
仍然是相同的例子,但是在TestDefer中我们定义了三个defer输出,根据LIFO原则,输出的顺序是3rd-2nd-1st,根据最后的结果,也是逆向向上执行defer输出。
就在整理这篇笔记的时候,发现了自己的认知误区,主要是本节实例三中发现的,先来看一下英文的描述:
对于上面的这段话的理解:
下面是代码执行输出,我们来一起分析一下:
虽然在a()函数内,显示的返回了10,但是main函数中得到的结果是defer函数自增后的结果,我们来分析一下代码:
在这篇文章的上一版,我曾经尝试用指针取解释defer修改返回值的类型,但是感觉不够透彻,也让阅读者非常困惑,索性参考了一下go官方blog中的一篇文章,在此基础上进行了扩展。如需要阅读原文,可以参考下面的文章。