Python调用Golang

Posted by Shi Hai's Blog on February 10, 2023

记一个工作中遇到的问题,由于要给C、Java、Go、Python提供客户端,客户端的核心逻辑实现语言是Go,所以Python客户端最快的实现方式是直接封装Go语言逻辑,而且因为业务需要,还要提供Python3/2两个版本,先有Python3版本,后有Python2版本,因此还需要考虑在Python2上执行Python3代码的兼容性问题。 主要的解决思路是Go语言和Python语言都对底层C语言的支持粒度较好,那就通过C语言作为中间语言,将Go语言和Python语言桥接在一起。

一、Go语言编译

考虑如下Go语言代码,定义了四个函数,func main()函数是必须的,否则会提示一个错误:runtime.main_main·f: function main is undeclared in the main package。将此文件保存至exports.go中,下文会继续使用到。

package main

import "C"
import "fmt"

//export Hello
func Hello() {
        fmt.Println("Hi!")
}

//export Say
func Say(content string) {
        fmt.Println("I saied:", content)
}

//export Greet
func Greet(name string) string {
        return fmt.Sprintln("Nice to meet you, ", name)
}

func main() {
        Hello()
}

这里的Go代码用了2处写Go代码不太常用的点:

  1. 导入C模块:启用CGO特性,用于Go语言能调用C语言函数库;
  2. 使用export注释:将相关函数导出到*.h头文件中。

1.1 构建静态链接

执行如下go语言构建命令后,你会在你的文件目录中发现多了两个文件:exports.aexports.h。这两个文件就是编译构建exports.go文件得到的静态文件和头文件。

go build -buildmode=c-archive exports.go

exports.h文件中,你会发现你定义的三个函数,如下所示:

#ifdef __cplusplus
extern "C" {
#endif

extern void Hello();
extern void Say(GoString content);
extern GoString Greet(GoString name);

#ifdef __cplusplus
}
#endif

1.2 构建动态链接

执行如下命令即可以编译出静态链接库,在你的构建目录中会多出两个文件:exports.hexports.so

go build --buildmode=c-shared -o exports.so exports.go

二、Python语言调用链接库

Pythong中的ctypes模块只提供了调用动态链接库的功能,所以想用Python加载静态链接的同学可以参考附录一进行扩展阅读。

2.1 通过ctypes调用链接库

在Python3解释器上,执行如下Python语言代码即可以判断动态链接库是否被正确加载。

import ctypes
# 加载动态链接库
exqports_lib = ctypes.cdll.LoadLibrary("./exports.so")
# 执行Go语言中定义的Hello()函数
exports_lib.Hello()

但继续执行其他函数时就会触发Core Dump异常错误,执行Say()函数的报错如下所示。

>>> exports_lib.Say("Hi")
runtime: out of memory: cannot allocate 140732241281024-byte block (3833856 in use)
fatal error: out of memory

goroutine 17 [running, locked to thread]:
runtime.throw({0x7f1e3b643c7b?, 0x7f1e3b91ad40?})
runtime.(*mcache).allocLarge(0x7f1e3b5d1f50?, 0x7ffec707c000, 0x1)
        /usr/lib/golang/src/runtime/mcache.go:235 +0x205 fp=0xc00005eaa0 sp=0xc00005ea50 pc=0x7f1e3b5d3e45
runtime.mallocgc(0x7ffec707c000, 0x0, 0x0)
        /usr/lib/golang/src/runtime/malloc.go:1029 +0x57e fp=0xc00005eb18 sp=0xc00005eaa0 pc=0x7f1e3b5cbdbe
...
[5]    369280 abort (core dumped)  python3

这里报了一个goroutine协程的错误,但实际我没有调用goroutine协程,这里我个人盲测Go语言中的CGO特性依赖了goroutine模块,后续有时间在展开分析。
实际上方报错的原因在于调用Go中的Say()函数的入参是不正确的。在exports.h文件中你可以看见Say()函数的入参是一个GoString类型,而且GoString是一个含有两个属性的结构体。但我们在Python语言中调用传入的仅是一个字符串,所以会发生错误。

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif

typedef _GoString_ GoString;
extern void Say(GoString content);

此时需要将Python代码适配修改为如下代码:

import ctypes


class GoString(ctypes.Structure):
    _fields_ = [
            ("p", ctypes.c_char_p),
            ("n", ctypes.c_int64)
            ]


exports_lib = ctypes.cdll.LoadLibrary("./exports.so")
exports_lib.Hello()

exports_lib.Say.argtypes = [GoString]
contents = "Hi"
exports_lib.Say(GoString(contents.encode(), len(contents)))

重新用Python3解释器运行会发现,代码能再一次被重新执行了,执行结果输出如下所示:

Hi!
I saied: Hi

2.2 Python2上执行Python3代码兼容问题

2.2.1 bytes类型

bytes是Python3中新的特性,在Python2中没有此类型。上文中的这个示例代码在Python2中能兼容执行。

import ctypes


class GoString(ctypes.Structure):
    _fields_ = [
            ("p", ctypes.c_char_p),
            ("n", ctypes.c_int64)
            ]


exports_lib = ctypes.cdll.LoadLibrary("./exports.so")
exports_lib.Hello()

exports_lib.Say.argtypes = [GoString]
contents = "Hi"
exports_lib.Say(GoString(contents.encode(), len(contents)))

实际在Python2中执行这个示例时不需要执行contents.encode(),即直接执行:

exports_lib.Say.argtypes = [GoString]
contents = "Hi"
exports_lib.Say(GoString(contents, len(contents)))
# exports_lib.Say(GoString(contents.encode(), len(contents)))

因为在Python2和Python3中,ctypes.c_char_p对应的Python的兼容数据类型发生了变化。在Python3中,bytes或者None兼容ctypes.c_char_p类型,而在Python2中则是str或者None兼容ctypes.c_char_p类型。

2.2.2 unicode类型

Python3中字符串(str)是一个unicode类型,而在Python2中,字符串(str)和unicode对象是两种类型。

三、参考附录

1.在Python中调用Go
2.Go build模式之c-archive,c-shared,linkshared
3.Go CGO
4.Python Ctypes
5.Go语言之父:热爱冒险,发明过航天望远镜,想用Go语言解放程序员!
6.Python3 ctypes
7.Python2 ctypes