はじめに (対象読者・この記事でわかること)

この記事は、「Go言語で開発しているけれど、Pythonの豊富なライブラリを活用したい」「GoとPythonを連携させて、より高度なアプリケーションを開発したい」と考えているエンジニアの方々を対象としています。

この記事を読むことで、あなたは以下のことを理解し、実践できるようになります。

  • go-pythonライブラリの基本的な使い方
  • GoからPythonの関数を呼び出し、結果を受け取る方法
  • Pythonのリストや辞書などのデータ構造をGoで扱う方法
  • 簡単なGo-Python連携アプリケーションの構築例

Go言語のパフォーマンスとPythonの柔軟性を組み合わせることで、開発の幅が大きく広がります。ぜひ、この機会にgo-pythonによる異言語連携の可能性を探ってみましょう。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * Go言語の基本的な文法と開発経験 * Pythonの基本的な文法と開発経験 * コマンドラインインターフェース(CLI)の基本的な操作

GoからPythonを呼び出す!go-pythonの概要と導入

近年、マイクロサービスアーキテクチャの普及や、特定のタスクに最適な言語を選択する「ポリグロット・プログラミング」の考え方が広まっています。Go言語は、その高いパフォーマンスと並行処理能力からバックエンド開発で広く利用されていますが、一方でPythonは、機械学習、データサイエンス、Webスクレイピングなど、特定の分野で強力なエコシステムと豊富なライブラリを持っています。

この両方の言語の強みを活かすために、GoからPythonコードを呼び出すことができれば、開発の効率とアプリケーションの機能を大幅に向上させることが可能です。そこで活躍するのが、go-python というライブラリです。

go-python は、GoプログラムからPythonインタープリタを埋め込み、Pythonコードを実行するためのライブラリです。これにより、Goのコード内からPythonの関数を呼び出したり、Pythonで書かれたライブラリを利用したりすることが可能になります。

go-pythonのインストール

go-python を利用するには、まずGoのモジュールとしてインポートできるように、以下のコマンドでインストールします。

Bash
go get github.com/sbinet/go-python

また、go-python は内部でPythonインタープリタを利用するため、システムにPythonがインストールされている必要があります。Python 3.x 系が推奨されています。インストールされていない場合は、お使いのOSに合わせてPythonをインストールしてください。

Python環境の設定

go-python は、GoプログラムからどのPython環境を利用するかを認識させる必要があります。通常は、python3.dll (Windows) や libpython3.so (Linux), libpython3.dylib (macOS) といったPythonの共有ライブラリのパスを指定します。

環境変数 PYTHONHOMEPYTHONPATH を設定するか、Goコード内で明示的にパスを指定する方法がありますが、簡単なのは、Goコード内で python.Initialize() を呼び出す際に、Pythonの共有ライブラリのパスを渡す方法です。

例えば、Linux環境でPython 3.9が /usr/lib/python3.9 にインストールされている場合、以下のように初期化できます。

Go
import "github.com/sbinet/go-python" // ... func main() { // Pythonの共有ライブラリのパスを指定して初期化 // 例: "/usr/lib/python3.9/lib-dynload" のようなパスが必要な場合もあります。 // 環境によってパスは異なりますので、ご自身の環境に合わせて調整してください。 err := python.Initialize() if err != nil { panic(err.Error()) } defer python.Finalize() // プログラム終了時にPythonインタープリタをクリーンアップ // ... Pythonコードの実行 ... }

Pythonの共有ライブラリのパスは、sysconfig.get_config_var('LIBDIR')sysconfig.get_config_var('INSTSONAME') などで確認できます。Goコード内からこれらの情報を取得して動的に設定することも可能ですが、まずは手動でパスを指定するのが理解しやすいでしょう。

go-pythonを使ったPythonコードの実行とデータ連携

go-python の基本的な導入方法が理解できたところで、実際にGoからPythonコードを実行し、データのやり取りを行う方法を学びましょう。

Pythonモジュールのインポートと関数の呼び出し

GoからPythonコードを実行する最も基本的な方法は、Pythonのモジュールをインポートし、その中の関数を呼び出すことです。

まず、呼び出したいPythonコードを記述したファイルを作成します。例えば、my_module.py という名前で以下の内容を保存します。

Python
# my_module.py def greet(name): return f"Hello, {name} from Python!" def add(a, b): return a + b

次に、この my_module.py をGoプログラムから呼び出します。

Go
package main import ( "fmt" "github.com/sbinet/go-python" ) func main() { // Pythonインタープリタの初期化 err := python.Initialize() if err != nil { panic(fmt.Sprintf("Failed to initialize Python: %v", err)) } defer python.Finalize() // Pythonのsysモジュールを取得 sys := python.PyImport_ImportModule("sys") if sys == nil { panic("Failed to import sys module") } // sys.pathにカレントディレクトリを追加して、my_module.pyを見つけられるようにする // (my_module.pyがGoプログラムと同じディレクトリにある場合) currentDir, err := os.Getwd() if err != nil { panic(fmt.Sprintf("Failed to get current directory: %v", err)) } sysPath := sys.GetAttrString("path") sysPath.Append(python.PyString_FromString(currentDir)) // Pythonモジュールのインポート myModule := python.PyImport_ImportModule("my_module") if myModule == nil { // エラーが発生した場合、Pythonのエラーを表示する python.PyErr_Print() panic("Failed to import my_module") } // greet関数を呼び出す fmt.Println("--- Calling greet function ---") greetFunc := myModule.GetAttrString("greet") if greetFunc == nil { panic("Failed to get greet function from my_module") } // 関数に引数を渡す args := python.PyTuple_New(1) args.SetItem(0, python.PyString_FromString("Go Developer")) // 関数を実行し、結果を取得する result := greetFunc.CallObject(args) if result == nil { python.PyErr_Print() panic("Failed to call greet function") } // 結果をGoの文字列に変換して表示 fmt.Printf("Result from Python greet: %s\n", python.PyString_AsString(result)) // add関数を呼び出す fmt.Println("\n--- Calling add function ---") addFunc := myModule.GetAttrString("add") if addFunc == nil { panic("Failed to get add function from my_module") } // add関数に2つの数値引数を渡す addArgs := python.PyTuple_New(2) addArgs.SetItem(0, python.PyInt_FromInt(10)) addArgs.SetItem(1, python.PyInt_FromInt(25)) addResult := addFunc.CallObject(addArgs) if addResult == nil { python.PyErr_Print() panic("Failed to call add function") } // 結果をGoの整数に変換して表示 fmt.Printf("Result from Python add: %d\n", python.PyInt_AsInt(addResult)) }

このコードを実行するには、my_module.py と同じディレクトリで go run main.go を実行します。 実行結果は以下のようになります。

--- Calling greet function ---
Result from Python greet: Hello, Go Developer from Python!

--- Calling add function ---
Result from Python add: 35

Pythonのデータ構造をGoで扱う

go-python を使うと、Pythonのリストや辞書といったデータ構造もGo側で扱うことができます。

PythonのリストをGoで受け取る例:

まず、Python側でリストを返す関数を作成します。

Python
# my_list_module.py def get_numbers(): return [1, 2, 3, 4, 5] def get_strings(): return ["apple", "banana", "cherry"]

Go側では、Pythonのリストオブジェクトを取得し、それをGoのスライスに変換します。

Go
package main import ( "fmt" "github.com/sbinet/go-python" "os" ) func main() { err := python.Initialize() if err != nil { panic(fmt.Sprintf("Failed to initialize Python: %v", err)) } defer python.Finalize() // sys.path にカレントディレクトリを追加 sys := python.PyImport_ImportModule("sys") currentDir, _ := os.Getwd() sysPath := sys.GetAttrString("path") sysPath.Append(python.PyString_FromString(currentDir)) // my_list_module のインポート myListModule := python.PyImport_ImportModule("my_list_module") if myListModule == nil { python.PyErr_Print() panic("Failed to import my_list_module") } // get_numbers 関数を呼び出し、リストを取得 fmt.Println("--- Getting numbers from Python list ---") getNumbersFunc := myListModule.GetAttrString("get_numbers") if getNumbersFunc == nil { panic("Failed to get get_numbers function") } pyListNumbers := getNumbersFunc.CallObject(nil) // 引数なし if pyListNumbers == nil { python.PyErr_Print() panic("Failed to call get_numbers function") } // PythonリストをGoのスライスに変換 // PyList_Check でリストであることを確認 if python.PyList_Check(pyListNumbers) { listLen := python.PyList_Size(pyListNumbers) goNumbers := make([]int, listLen) for i := 0; i < listLen; i++ { item := python.PyList_GetItem(pyListNumbers, i) if item == nil { panic("Failed to get item from Python list") } // PyInt_AsInt で整数に変換 goNumbers[i] = python.PyInt_AsInt(item) } fmt.Printf("Go slice from Python list: %v\n", goNumbers) } else { fmt.Println("Result is not a Python list.") } // get_strings 関数を呼び出し、文字列リストを取得 fmt.Println("\n--- Getting strings from Python list ---") getStringsFunc := myListModule.GetAttrString("get_strings") if getStringsFunc == nil { panic("Failed to get get_strings function") } pyListStrings := getStringsFunc.CallObject(nil) if pyListStrings == nil { python.PyErr_Print() panic("Failed to call get_strings function") } if python.PyList_Check(pyListStrings) { listLen := python.PyList_Size(pyListStrings) goStrings := make([]string, listLen) for i := 0; i < listLen; i++ { item := python.PyList_GetItem(pyListStrings, i) if item == nil { panic("Failed to get item from Python list") } // PyString_AsString で文字列に変換 goStrings[i] = python.PyString_AsString(item) } fmt.Printf("Go slice from Python string list: %v\n", goStrings) } else { fmt.Println("Result is not a Python list.") } }

実行結果は以下のようになります。

--- Getting numbers from Python list ---
Go slice from Python list: [1 2 3 4 5]

--- Getting strings from Python list ---
Go slice from Python string list: [apple banana cherry]

Pythonの辞書をGoで受け取る例:

Python
# my_dict_module.py def get_config(): return { "host": "localhost", "port": 8080, "debug": True }
Go
package main import ( "fmt" "github.com/sbinet/go-python" "os" ) func main() { err := python.Initialize() if err != nil { panic(fmt.Sprintf("Failed to initialize Python: %v", err)) } defer python.Finalize() // sys.path にカレントディレクトリを追加 sys := python.PyImport_ImportModule("sys") currentDir, _ := os.Getwd() sysPath := sys.GetAttrString("path") sysPath.Append(python.PyString_FromString(currentDir)) // my_dict_module のインポート myDictModule := python.PyImport_ImportModule("my_dict_module") if myDictModule == nil { python.PyErr_Print() panic("Failed to import my_dict_module") } // get_config 関数を呼び出し、辞書を取得 fmt.Println("--- Getting config from Python dict ---") getConfigFunc := myDictModule.GetAttrString("get_config") if getConfigFunc == nil { panic("Failed to get get_config function") } pyDictConfig := getConfigFunc.CallObject(nil) if pyDictConfig == nil { python.PyErr_Print() panic("Failed to call get_config function") } // Python辞書をGoのマップに変換 // PyDict_Check で辞書であることを確認 if python.PyDict_Check(pyDictConfig) { // Python辞書のキーを取得 keys := python.PyDict_Keys(pyDictConfig) if keys == nil { panic("Failed to get keys from Python dict") } goConfig := make(map[string]interface{}) numKeys := python.PyList_Size(keys) // Dict_Keysはリストを返す for i := 0; i < numKeys; i++ { keyObj := python.PyList_GetItem(keys, i) if keyObj == nil { panic("Failed to get key item from Python dict keys") } keyStr := python.PyString_AsString(keyObj) // キーは文字列として取得 valueObj := python.PyDict_GetItem(pyDictConfig, keyObj) // キーオブジェクトを使って値を取得 if valueObj == nil { panic(fmt.Sprintf("Failed to get value for key '%s'", keyStr)) } // 値の型を判定して適切に変換 if python.PyInt_Check(valueObj) { goConfig[keyStr] = python.PyInt_AsInt(valueObj) } else if python.PyString_Check(valueObj) { goConfig[keyStr] = python.PyString_AsString(valueObj) } else if python.PyBool_Check(valueObj) { goConfig[keyStr] = python.PyInt_AsInt(valueObj) == 1 // PythonのTrue/Falseは1/0として扱われる } else { // その他の型は必要に応じて処理を追加 goConfig[keyStr] = fmt.Sprintf("Unsupported type: %T", valueObj) } } fmt.Printf("Go map from Python dict: %v\n", goConfig) } else { fmt.Println("Result is not a Python dict.") } }

実行結果は以下のようになります。

--- Getting config from Python dict ---
Go map from Python dict: map[debug:1 host:localhost port:8080]

True1 として取得される点に注意してください。必要に応じてGo側で bool 型に変換してください。

ハマった点やエラー解決

go-python を利用する上で、いくつかのハマりやすいポイントがあります。

  • Pythonの共有ライブラリパス: 最も頻繁に遭遇する問題は、GoプログラムがPythonインタープリタを見つけられないことです。python.Initialize() の引数や環境変数 PYTHONHOME の設定を正確に行う必要があります。OSやPythonのインストール方法によってパスは異なるため、sysconfig モジュールなどを活用して動的にパスを取得・設定する方法を検討すると良いでしょう。
  • GIL (Global Interpreter Lock): PythonにはGILという仕組みがあり、一度に一つのスレッドしかPythonバイトコードを実行できません。Goの並行処理 (goroutine) と組み合わせる際に、意図しないブロックが発生する可能性があります。go-python はGILを管理しますが、パフォーマンスに影響を与える場合があるため、注意が必要です。
  • 型変換: GoとPythonの間でデータをやり取りする際に、型変換は必須です。go-python は多くの型変換関数を提供していますが、複雑なデータ構造やカスタムクラスのやり取りは、より丁寧な実装が必要になります。Py_TYPE() などで型をチェックし、安全に変換することが重要です。
  • エラーハンドリング: Pythonコードの実行中にエラーが発生した場合、go-python を通してエラーを受け取ることができます。python.PyErr_Print() はPythonのエラースタックトレースを表示してくれるため、デバッグに非常に役立ちます。Go側で適切にエラーをチェックし、処理することが重要です。

解決策

これらの問題に対処するためには、以下の点が重要です。

  • ドキュメントの熟読: go-python の公式ドキュメントやGitHubリポジトリのREADMEをよく読み、提供されているAPIを理解することが重要です。
  • サンプルコードの参照: GitHubリポジトリにあるサンプルコードを参考に、実際の使い方を学ぶのが効果的です。
  • デバッグ: fmt.Printlnpython.PyErr_Print() を活用して、プログラムの実行状況やエラーメッセージを細かく確認し、問題箇所を特定します。
  • Pythonの標準ライブラリの活用: GoからPythonの標準ライブラリ(sys, os など)をインポートして、パスの確認や環境情報の取得などに活用することで、より柔軟な連携が可能になります。
  • 単体テスト: PythonコードとGoコードの連携部分について、単体テストを作成することで、予期せぬバグの混入を防ぎ、コードの信頼性を高めることができます。

まとめ

本記事では、Go言語からPythonコードを呼び出すためのライブラリである go-python の使い方について解説しました。

  • go-python を利用することで、GoプログラムにPythonの豊富なライブラリや既存のPythonコードを組み込むことができます。
  • Pythonモジュールのインポート、関数の呼び出し、引数の受け渡し、そして戻り値の受け取り方について具体的なコード例を示しました。
  • Pythonのリストや辞書といったデータ構造をGo側でどのように扱えるかについても説明しました。
  • go-python を利用する上での注意点や、よくある問題とその解決策についても触れました。

この記事を通して、GoとPythonという異なる言語の強みを組み合わせることで、より強力で柔軟なアプリケーションを開発できる可能性を感じていただけたかと思います。今後は、機械学習モデルの推論をGoから呼び出す、Goの高速な処理でPythonスクリプトを制御するなど、さらに応用的な使い方について記事にする予定です。

参考資料