1. 程式人生 > >Golang 調用 Python 代碼

Golang 調用 Python 代碼

fde linker ecs werror -- 輸出 html 導入 操作

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

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

接下來進入正題。

1. 針對 python 版本安裝 python-dev

sudo apt install python3.6-dev

系統未默認安裝 python3.x 的開發環境,所以假如要通過 cgo 調用 python,需要安裝對應版本的開發包。

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

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

python3.6-config --cflags

python3.6-config --ldflags

以下是對應的輸出

-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 代碼的配置為

//#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. 部分示例代碼

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* 的方法都是由 cgo 模塊編譯調用的,這些方法也是 python 暴露的 C-API,而這裏的示例就到此為止,其他諸如調用 python 模塊方法的功能文檔裏也描述得十分詳細,盡管實施起來仍然有些麻煩。

但是請註意 C-API 的 2.x 與 3.x 版本仍有不同,比如 2.x 版本中的字符串操作類型 PyString_* 在 3.x 中便被重命名為 PyBytes_*

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

實際工作中,語言的互操作場景確實很讓人感覺頭疼,而 cgo 的文檔資料其實並不多,所以希望本文能給大家帶來一些幫助。

Golang 調用 Python 代碼