美文网首页pythongo
Python和Go:第二部分-使用Go扩展Python

Python和Go:第二部分-使用Go扩展Python

作者: 豆腐匠 | 来源:发表于2020-07-15 10:46 被阅读0次

    原文地址:https://www.ardanlabs.com/blog/2020/07/extending-python-with-go.html

    介绍

    在上一篇文章中我们了解了Go服务如何使用gRPC调用Python服务。使用gRPC将Go和Python程序连在一起是一个不错的选择,但是随之而来的是复杂性。你需要管理不止一项服务,部署变得更加复杂,并且你需要对每项服务进行监视和警报。与单应用程序相比,复杂度要高一个量级。

    在这篇文章中,我们将通过在Go中编写一个Python程序可以直接使用的共享库来降低使用gRPC的复杂性。使用这种方法,无需进行网络连接,也无需依赖数据类型,也无需编组。在从Python的共享库中调用函数的几种方法中,我们决定选用Python的ctypes模块。

    *注意:ctypes 在后台使用libffi。如果你想阅读一些令人恐惧的C代码-那就跳转到git库并开始阅读吧。:) *

    我还将展示我的工作流程,这也是提高工作效率的重要因素之一。我们将首先编写“纯” Go代码,然后编写代码以将其导出到共享库。然后,我们将切换到Python环境,并使用Python的交互式提示来执行代码。我们甚至可以使用在交互式提示中学到的知识来编写Python模块。

    示例:并行检查多个文件的数字签名

    假设你有一个包含数据文件的目录,并且需要验证这些文件的完整性。该目录包含一个sha1sum.txt文件,具有每个文件的sha1数字签名。Go具有并发原语和使用计算机所有内核的能力,比Python更适合此任务。

    sha1sum.txt

    6659cb84ab403dc85962fc77b9156924bbbaab2c  httpd-00.log
    5693325790ee53629d6ed3264760c4463a3615ee  httpd-01.log
    fce486edf5251951c7b92a3d5098ea6400bfd63f  httpd-02.log
    b5b04eb809e9c737dbb5de76576019e9db1958fd  httpd-03.log
    ff0e3f644371d0fbce954dace6f678f9f77c3e08  httpd-04.log
    c154b2aa27122c07da77b85165036906fb6cbc3c  httpd-05.log
    28fccd72fb6fe88e1665a15df397c1d207de94ef  httpd-06.log
    86ed10cd87ac6f9fb62f6c29e82365c614089ae8  httpd-07.log
    feaf526473cb2887781f4904bd26f021a91ee9eb  httpd-08.log
    330d03af58919dd12b32804d9742b55c7ed16038  httpd-09.log
    
    

    这个文件为目录中包含的所有不同日志文件提供哈希码。此文件可用于验证日志文件是否已正确下载或未被篡改。我们将编写Go代码来计算每个日志文件的哈希码,然后将其与数字签名文件中列出的哈希码进行匹配。

    为了加快此过程,我们将在单独的goroutine中计算每个文件的数字签名,将工作分散到计算机上的所有CPU中。

    体系结构概述和工作计划

    在代码的Python端,我们将编写一个名为的check_signatures函数,在Go端,我们将编写一个名为CheckSignatures的(实际执行工作的)函数。在这两个函数之间,我们将使用ctypes模块(在Python端)并编写一个verify函数(在Go端)以提供数据处理支持。

    图1

    image.png

    图1显示了从Python函数到Go函数以及返回的数据流。

    本文剩余部分将会遵循如下的步骤:

    • 编写Go代码(CheckSignature),
    • 导出到共享库(verify
    • 在Python交互式提示中使用ctypes调用Go代码
    • 编写并打包Python代码(check_signatures
    • 我们将在下一篇博客文章中详细描述最后的部分。

    Go代码-“ CheckSignatures”功能

    我不会在这里分解所有Go源代码,如果您好奇地看到所有这些,请查看此源代码文件

    现在要看的代码的重要部分是CheckSignatures函数的定义。

    CheckSignatures函数定义

    // CheckSignatures calculates sha1 signatures for files in rootDir and compare
    // them with signatures found at "sha1sum.txt" in the same directory. It'll
    // return an error if one of the signatures don't match
    func CheckSignatures(rootDir string) error {
    
    

    此函数将为每个文件启动一个goroutine,以检查所计算的给定文件的sha1签名是否与“ sha1sum.txt”中的签名相匹配。如果一个或多个文件不匹配,该函数将返回错误。

    将Go代码导出到共享库

    随着GO代码编写和测试的完成,我们可以继续将其导出到共享库。

    我们将按照以下步骤将Go源代码编译成共享库,以便Python可以调用它:

    • 导入C包(又名cgo
    • 在我们需要公开的每个函数上使用//export指令
    • 具有空main功能
    • 使用特殊的-buildmode=c-shared标志构建源代码

    注意:除了Go工具链之外,我们还需要C编译器(例如gcc)。每个主要平台都有一个不错的C编译器:比如Linux上的gcc,OSX上的clang和在Windows上的Visual Studio

    export.go

    01 package main
    02 
    03 import "C"
    04 
    05 //export verify
    06 func verify(root *C.char) *C.char {
    07  rootDir := C.GoString(root)
    08  if err := CheckSignatures(rootDir); err != nil {
    09      return C.CString(err.Error())
    10  }
    11 
    12  return nil
    13 }
    14 
    15 func main() {}
    
    

    我们先在第03行导入“ C”,然后在第05行加上verify,标记为要在共享库中导出。完全按原样使用备注很重要。可以在第06行看到,该verify函数使用C包char类型接受基于C的字符串指针。为了使Go代码能够使用C字符串,C程序包提供了一个GoString函数(在第07行中使用)和一个CString函数(在第09行中使用)。最后,在最后声明一个空main函数。

    要构建共享库,需要使用特殊标志运行go build命令。

    构建共享库

    $ go build -buildmode=c-shared -o _checksig.so
    
    

    注意:使用的原因_是为了避免与稍后将显示的checksig.py的Python模块发生名称冲突。如果共享库被命名为checksig.so,则import checksig在Python中执行将加载共享库而不是Python文件。

    准备测试数据

    在尝试从Python 调用verify之前,我们需要一些数据。在代码存储库中找到一个名为logs的目录。该目录包含一些日志文件和一个sha1sum.txt文件。

    注意:http08.log的签名是故意设置为错误的。**

    在我的机器上,该目录位于/tmp/logs

    Python会话

    我喜欢Python中的交互式操作,它使我可以小段地处理代码。

    Python会话

    $ python
    Python 3.8.3 (default, May 17 2020, 18:15:42) 
    [GCC 10.1.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    
    01 >>> import ctypes
    02 >>> so = ctypes.cdll.LoadLibrary('./_checksig.so')
    03 >>> verify = so.verify
    04 >>> verify.argtypes = [ctypes.c_char_p]
    05 >>> verify.restype = ctypes.c_void_p
    06 >>> free = so.free
    07 >>> free.argtypes = [ctypes.c_void_p]
    08 >>> ptr = verify('/tmp/logs'.encode('utf-8'))
    09 >>> out = ctypes.string_at(ptr)
    10 >>> free(ptr)
    11 >>> print(out.decode('utf-8'))
    12 "/tmp/logs/httpd-08.log" - mismatch
    

    上面的代码将引导你测试导出到我们在Go中编写的共享库。第01行,我们导入ctypes模块。然后在第06行,将共享库加载到内存中。在03-05行中,我们从共享库中加载verify函数并设置输入和输出类型。06-07行加载了free函数,因此我们可以释放Go分配的内存(详情参见下文)。

    第08行是对verify的实际函数调用。我们需要将目录的名称转换为python的bytes。返回值是C字符串,存储在ptr中。在第09行,我们将C字符串转换为Python的bytes,在第10行,我们释放了Go分配的内存。最后,在第11行,我们在打印之前将outbytes转换为str

    注意:第02行假定共享库_checksig.so在当前目录中。如果你在其他地方启动了Python会话,请将路径更改为_checksig.so

    只需这些操作,我们就可以从Python调用Go代码。

    Intermezzo:在Go和Python之间的共享内存

    Python和Go都有一个垃圾收集器,可以自动释放未使用的内存。但是,拥有垃圾收集器并不意味着不会出现内存泄露。

    注意:你应该阅读一下Bill的Go中的垃圾回收。可以帮助你对Go垃圾收集器有一个很好的了解。

    在Go和Python(或C)之间使用共享内存时,你需要格外小心。有时不清楚何时进行内存分配。在export.go第13行,我们有以下代码:

    将Go Error转换为C字符串

    str := C.CString(err.Error())
    

    cgo的文档中关于C.String的描述:

    > // Go string to C string
    > // The C string is allocated in the C heap using malloc.
    > // **It is the caller's responsibility to arrange for it to be
    > // freed**, such as by calling C.free (be sure to include stdlib.h
    > // if C.free is needed).
    > func C.CString(string) *C.char
    
    

    为了避免在交互式提示中发生内存泄漏,我们加载了free函数并使用它释放了Go分配的内存。

    结论

    只需很少的代码,你就可以从Python中使用Go。与上一部分不同,它没有RPC步骤-意味着你不需要在每个函数调用中都处理参数,并且也不需要网络。从Python到C的调用比gRPC调用快得多。另一方面,你需要更加谨慎地进行内存管理,并且打包过程会变的更加复杂。

    注意:在我的计算机上,一个简单的基准测试 下gRPC函数调用的为128µs,而共享库调用为3.61µs,大约快了35倍。

    我希望你已经找到了这种编写代码的方式:首先是纯Go的编写,然后是导出,然后在交互式会话中对其进行尝试。我建议你下次编写一些代码时自己先尝试此工作流程。

    在下一部分中,我们将完成工作流程的最后一步,并将Python代码打包为模块。

    相关文章

      网友评论

        本文标题:Python和Go:第二部分-使用Go扩展Python

        本文链接:https://www.haomeiwen.com/subject/rvcacktx.html