go 中的 cgo 模塊可讓 go 無縫調用 c 或者 c++ 的代碼,而 python 自己就是個 c 庫,天然也能夠由 cgo 直接調用,前提是指定正確的編譯條件,如 Python.h 頭文件(),以及要連接的庫文件。本文以 Ubuntu 18.04 做爲開發和運行平臺進行演示。html

其實在使用 cgo 以前,筆者也考慮過使用 grpc 的方式。好比能夠將須要調用的 python 代碼包裝成一個 grpc server 端,而後再使用 go 編寫對應的 client 端,這樣考慮的前提是,go 調用 python 代碼原本就是解一時之困,並且引入語言互操做後,對於項目維護和開發成本控制都有不小的影響,若是直接使用 grpc 生成編程語言無感知的協議文件,未來不管是重構或使用其餘語言替換 python 代碼,都是更加方便,也是更加解耦的。因此 grpc 也是一種比較好的選擇。至於通訊延遲,老實說既然已經設計語言互操做,本機中不到毫秒級的損失其實也是能夠接受的。python

接下來進入正題。linux

1. 針對 python 版本安裝 python-devc++

sudo apt install python3.6-dev

系統未默認安裝 python3.x 的開發環境,因此假如要經過 cgo 調用 python,須要安裝對應版本的開發包。git

2. 指定對應的cgo CFLAGS 和 LDFLAGS 選項github

對於未由 c 包裝的 python 代碼,python-dev 包中內置了 python-config 工具用於查看編譯選項。編程

python3.6-config --cflags

python3.6-config --ldflags

如下是對應的輸出python3.x

-I/usr/include/python3.6m -I/usr/include/python3.6m  -Wno-unused-result -Wsign-compare -g -fdebug-prefix-map=/build/python3.6-MtRqCA/python3.6-3.6.6=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector -Wformat -Werror=format-security  -DNDEBUG -g -fwrapv -O3 -Wall

-L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -L/usr/lib -lpython3.6m -lpthread -ldl  -lutil -lm  -xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions

低版本的 python 也能夠在安裝開發包後,使用對應的 python-config 命令打印依賴配置。因爲 cgo 默認使用的編譯器不是 gcc ,因此輸出中的部分選項並不受支持,因此最後 cgo 代碼的配置爲api

//#cgo CFLAGS : -I./ -I/usr/include/python3.6m
//#cgo LDFLAGS: -L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -L/usr/lib -lpython3.6m -lpthread -ldl  -lutil -lm
//#include "Python.h"
import "C"

3. 部分示例代碼python2.7

3.0 映射 PyObject

type PyObject struct {
    ptr *C.PyObject
}

func togo(obj *C.PyObject) *PyObject {
    if obj == nil {
        return nil
    }
    return &PyObject{ptr: obj}
}

func topy(self *PyObject) *C.PyObject {
    if self == nil {
        return nil
    }
    return self.ptr
}

3.1 python 環境的啓動與終結

func Initialize() error {
    if C.Py_IsInitialized() == 0 {
        C.Py_Initialize()
    }
    if C.Py_IsInitialized() == 0 {
        return fmt.Errorf("python: could not initialize the python interpreter")
    }

    if C.PyEval_ThreadsInitialized() == 0 {
        C.PyEval_InitThreads()
    }
    if C.PyEval_ThreadsInitialized() == 0 {
        return fmt.Errorf("python: could not initialize the GIL")
    }

    return nil
}

func Finalize() error {
    C.Py_Finalize()
    return nil
}

3.2 包路徑與模塊導入

func InsertExtraPackageModule(dir string) *PyObject {
    sysModule := ImportModule("sys")
    path := sysModule.GetAttrString("path")

    cstr := C.CString(dir)
    defer C.free(unsafe.Pointer(cstr))
    C.PyList_Insert(topy(path), C.Py_ssize_t(0), topy(togo(C.PyBytes_FromString(cstr))))

    return ImportModule(dir)
}

func ImportModule(name string) *PyObject {
    c_name := C.CString(name)
    defer C.free(unsafe.Pointer(c_name))
    return togo(C.PyImport_ImportModule(c_name))
}

func (self *PyObject) GetAttrString(attr_name string) *PyObject {
    c_attr_name := C.CString(attr_name)
    defer C.free(unsafe.Pointer(c_attr_name))
    return togo(C.PyObject_GetAttrString(self.ptr, c_attr_name))
}

3.3 數據類型轉換

func PyStringFromGoString(v string) *PyObject {
    cstr := C.CString(v)
    defer C.free(unsafe.Pointer(cstr))
    return togo(C.PyBytes_FromString(cstr))
}

func PyStringAsGoString(self *PyObject) string {
    c_str := C.PyBytes_AsString(self.ptr)
    return C.GoString(c_str)
}

...
C.Py*
PyString_*PyBytes_*

關注過 go 與 python 互操做功能的同窗應該注意到上述的示例代碼部分來自 go-python 這個開源項目,有興趣的同窗也能夠關注一下。 這個項目基於 python2.7 ,其中暴露的 api 諸如字符串轉換也是基於 python2.x 版本,因此針對於更流行的 python3.x 項目,你們就須要本身按照上文方法作一些修改了。

這裏還能夠介紹下 pkg-config 工具,在上文咱們提到了 python-config 工具,而 pkg-config 能夠幫助咱們簡化提取配置參數,go-python 項目也是這麼作的。咱們只須要將上文 cgo 配置代碼更改成如下形式便可。pkg-config 工具會自動查詢編譯配置和庫文件。

//#cgo pkg-config: python-3.6
//#include "Python.h"
import "C"

實際工做中,語言的互操做場景確實很讓人感受頭疼,而 cgo 的文檔資料其實並很少,因此但願本文能給你們帶來一些幫助。