如果你看一下新的DatadogAgent,你可能会注意到大部分代码库都是用Go编写的,尽管我们用来收集指标的检查仍然是用Python编写的。这大概是因为DatadogAgent是一个普通的Go二进制文件,带有一个嵌入式CPython解释器,可以随时按需执行Python代码。这个过程通过抽象层变得透明,允许您在底层运行Python的同时编写惯用的Go代码。在Go应用程序中嵌入Python有几个原因:它在过渡期间很有用;现有Python项目的一部分可以逐渐迁移到新语言,而不会在此过程中丢失任何功能。您可以重用现有的Python软件或库,而无需使用新语言重新实现它们。即使在运行时,您也可以通过加载和执行常规Python脚本来动态扩展您的软件。原因可以继续下去,但对于DatadogAgent来说,最后一点至关重要:我们希望能够执行自定义检查或更改现有检查,而无需重新编译Agent,或根本不编译任何东西。嵌入CPython很容易,而且有据可查。解释器本身是用C编写的,并提供CAPI以编程方式执行低级操作,例如创建对象、导入模块和调用函数。在本文中,我们将展示一些代码示例,我们将在与Python交互的同时继续保持Go代码的惯用语,但在继续之前,我们需要解决一个问题:嵌入API在C中,但我们的main应用程序是Go,这怎么可能工作?引入cgo有很多很好的理由说明您不应该在堆栈中包含cgo,但是嵌入CPython是您必须这样做的原因。cgo不是语言,也不是编译器。它就是外部函数接口(FFI),一种我们可以在Go中调用不同语言(尤其是C)编写的函数和服务的机制。当我们说“cgo”时,我们实际上指的是Go工具链在底层使用的一组工具、库、函数和类型,因此我们可以通过执行gobuild来获取我们的Go二进制文件。以下是使用cgo的示例程序:packagemain//#includeimport"C"import"fmt"funcmain(){fmt.Println("float的最大浮点值是",C.FLT_MAX)在这种包含头文件的情况下,导入“C”指令上方的注释块称为“序言”,可以包含实际的C代码。导入后,我们可以通过“C”伪包“跳转”到外部代码来访问常量FLT_MAX。你可以通过调用gobuild来构建,就像普通的Go一样。如果你想确切地看到cgo在幕后做了什么,你可以运行gobuild-x。你会看到将调用“cgo”工具来生成一些C和Go模块,然后将调用C和Go编译器来构建目标模块,最后链接器将所有内容放在一起。您可以在Go博客上阅读更多关于cgo的信息,其中包含更多示例和一些有用的链接以获取更多详细信息。现在我们已经了解了cgo可以为我们做什么,让我们看看如何使用这种机制来运行一些Python代码。嵌入CPython:入门指南从技术上讲,使用CPython嵌入Go程序并不像您想象的那么复杂。事实上,我们只是在运行Python代码之前初始化解释器,并在完成后关闭它。请注意,我们在所有示例中都使用Python2.x,但我们可以通过很少的调整应用到Python3.x。让我们看一个例子:packagemain//#cgopkg-config:python-2.7//#includeimport"C"import"fmt"funcmain(){C.Py_Initialize()fmt.Println(C.GoString(C.Py_GetVersion()))C.Py_Finalize()}上面的例子和下面的Python代码做的完全一样:这些指令被传递到工具链,允许您更改构建工作流程。在这种情况下,我们告诉cgo调用pkg-config来收集构建和链接名为python-2.7的库所需的标志,并将这些标志传递给C编译器。如果你的系统上安装了CPython开发库和pkg-config,你只需要运行gobuild来编译上面的例子。回到代码中,我们使用Py_Initialize()和Py_Finalize()初始化和关闭解释器,并使用Py_GetVersionC函数获取一串嵌入式解释器版本信息。如果您想知道,我们需要放在一起以从C调用PythonAPI的所有cgo代码都是样板代码。这就是DatadogAgent依赖go-python进行所有嵌入操作的原因;该库为CAPI提供了一个对Go友好的轻量级包,并隐藏了cgo的细节。这是另一个基本的嵌入式示例,这次使用的是go-python:'")python.Finalize()}这看起来更接近纯Go代码,不再暴露cgo,我们可以在访问PythonAPI时来回使用Go字符串。Embedded看起来功能强大且对开发人员友好,是时候充分利用解释器了:让我们尝试从磁盘加载Python模块。在Python方面我们不需要任何复杂的东西,无处不在的“helloworld”就可以做到这一点:!”Go代码稍微复杂一些,但仍然可读://main.gopackagemainimport"github.com/sbinet/go-python"funcmain(){python.Initialize()deferpython.Finalize()fooModule:=python.PyImport_ImportModule("foo")iffooModule==nil{panic("导入模块时出错")}helloFunc:=fooModule.GetAttrString("hello")ifhelloFunc==nil{panic("导入函数时出错")}//Python函数不接受任何参数,但是在使用Capi时//我们无论如何都需要发送(空)*args和**kwargs。你好功能。Call(python.PyTuple_New(0),python.PyDict_New())}在构建的时候,我们需要将PYTHONPATH环境变量设置为当前工作目录,这样import语句才能找到foo.py模块。在shell中,命令如下所示:$gobuildmain.go&&PYTHONPATH=。./main你好,世界!为了嵌入Python,必须引入可怕的GlobalInterpreterLockcgo,这是一个权衡:构建会更慢,垃圾收集器不会帮助我们管理外部系统使用的内存,并且交叉编译很难.这些问题对于特定项目是否值得商榷,但我认为有一些不可协商的问题:Go并发模型。如果我们不能从goroutine运行Python,那么使用Go就没有意义。在处理并发、Python和cgo之前,我们需要了解一个东西:它就是全局解释器锁,即GIL。GIL是一种在语言解释器(CPython是其中之一)中广泛采用的机制,可以防止多个线程并发运行。这意味着任何由CPython执行的Python程序都不能在同一个进程中并行运行。并发仍然是可能的,锁是速度、安全性和易于实现之间的一个很好的权衡,那么为什么这在嵌入时会造成问题呢?当一个常规的非嵌入式Python程序启动时,GIL不参与以避免锁定操作中无用的开销;当一些Python代码首先要求产生一个线程时,GIL就会启动。对于每个线程,解释器创建一个数据结构来存储当前相关的状态信息并锁定GIL。当线程完成时,状态被恢复并且GIL被解锁,准备被其他线程使用。当我们从Go程序运行Python时,以上都不会自动发生。如果没有GIL,我们的Go程序可能会创建多个Python线程,这可能会导致竞争条件,导致致命的运行时错误,并且很可能会导致整个Go应用程序崩溃的分段错误。解决方案是在我们从Go运行多线程代码时显式调用GIL;代码并不复杂,因为CAPI提供了我们需要的所有工具。为了更好地暴露这个问题,我们需要编写一些CPU绑定的Python代码。让我们将这些函数添加到前面示例中的foo.py模块:#foo.pyimportsysdefprint_odds(limit=10):"""Printoddsnumbers