豪鬼メモ

一瞬千撃

Go言語勉強中

TkrzwのGo言語インターフェイスを鋭意製作中であるが、今更ながら、「プログラミング言語Go」を読んだ。20年前に愛読した「プログラミング言語C」や「プログラミング作法」や「UNIXプログラミング環境」のカーニハン先生が著者の一人なので、なんか懐かしい気分になった。Go言語の概要についてはこれで理解したのだが、その本で詳述されていなかった点でいくつかはまったので、メモしておく。

GoからC++言語のライブラリを直接呼ぶことはできないが、C言語の関数は直接呼ぶことができる。そのためにはcgoという機能を使う。本家のチュートリアルを読めば概要はつかめる。ユーザの環境に自動的にインストールできるようにするためには、"#cgo pkg-config" により、pkg-config経由でコンパイルやリンクのオプションが参照できるようにしておかねばならない。

Goオブジェクトのポインタをuintptrにキャストした場合、そのポインタは別の行でポインタとして使ってはならない。なぜなら、Go処理系のGCが該当のオブジェクトのアドレスを変更した際には、それを指すポインタも自動で更新してくれるが、uintptrの値はポインタではなく数値とみなされるので、GCによって更新されず、無効なポインタになるからだ。同じ行で使い切るなら良い。

一方、Cオブジェクトのポインタ(*FooBarやunsafe.Pointer)は、ポインタ型として保持し続けてはならない。Go処理系のGCは生きているゴルーチンのスタックにあるポインタを起点として参照可能なポインタを検索するので、Cオブジェクトのポインタを保持していると無駄な検索が発生するからだ。よって、Cオブジェクトのポインタは速やかにuintptrに変換して数値として保持すべきだ。

Cのスレッドローカル変数に複数のゴルーチンからアクセスすると、レースコンディションが起きる。単一のネイティブスレッド上で複数のゴルーチンがタイムシェアで切り替わって実行されるので、スレッドローカル変数の値を設定する関数呼び出しとその値を参照する関数がGoの個別の命令として書かれていると、その間にプリエンプションが起こり、別の変数設定が割り込むかもしれないからだ。それを防ぐには、スレッドローカル変数の設定とその参照は同一のC関数内で完結せねばならない。

Cで作った配列にGoからアクセスするのが大変だ。Cの配列は先頭要素のポインタと要素数で管理され、各要素の参照は先頭ポインタにオフセットを足して行われる。オフセットの算出の際には添字に要素サイズを掛け算する暗黙の変換が行われる。一方でGoではポインタの演算が許されておらず、Cオブジェクトのポインタでもそれは例外ではない。じゃあどうするかと言うと、unsafe.Pointerを介してuintptrに変換して、数値としてアドレスの演算を行う。uintptrは単なる数値であり、元のオブジェクトのサイズを知らないから、添字に要素サイズを掛け算する演算はプログラマの責任になる。そういうわけで、RecordというCの型の配列を全走査するには、以下のようなGoコードを書くことになる。

records, num_records := C.get_records(...)
rec_ptr := uintptr(unsafe.Pointer(records))
for i := C.int32_t(0); i < num_records; i++ {
  record := (*C.TkrzwKeyValuePair)(unsafe.Pointer(rec_ptr))
  ...  
  rec_ptr += unsafe.Sizeof(C.Record{})
}

新しめのGoのパッケージはGo Modulesという機能で管理される。パッケージをGitHub等の公開リポジトリに置いておけば、ビルド時に勝手にダウンロードしてインストールされるようになる。めちゃ便利だ。本家のチュートリアルを読めばだいたいのことが分かる。開発中のパッケージをパッケージの外から利用したい場合、go mod edit -replace github/estraier/tkrzw-go=../tkrzw-go のようなコマンドでgo.modファイルを書き換えて、ローカルファイルシステムを参照するシンボリックリンク的な措置をすると良い。ここで注意すべきは、GitHubにpushしたパッケージを使って何かする場合、自動ダウンロードされるバージョンが古い可能性があるということだ。参照するバージョンを@latestなどにしても、やはり古いものがダウンロードされることがある。プロクシ上にキャッシュされたパッケージのデータを取り込むからだ。それを回避するには、GOPRIVATE="github.com/estraier" などと環境変数を設定して、その接頭辞を持つパッケージをキャッシュから除外する必要がある。

現状のGoエコシステムでは、APIの文書化に関する標準的な手順が確立されていないっぽい。各種識別子につけるべきdoc commentのスタイルに関しては本家にガイドラインがあり、go doc Hogeなどとして閲覧する機能も整備されている。一方で、HTMLなどの構造化データを書き出す標準的な手段はない。godocという外部ツールがあり、Ubuntuだとgolang-golang-x-toolsパッケージとしてインストールできるが、そいつはWebブラウザでHTML文書を閲覧するためのWebサーバとして機能するが、HTML文書の書き出しはしてくれない。かつてはその機能があったが、メンテ上の理由で削除されたらしい。「CLIの方が慣れれば便利だよ」「毎回Webサーバを立てればいいよ」「pkg.go.devにアップロードすれば自分でWebサーバを立てなくてもよくなるよ」といった意見が巷にはあるらしいが、自分には腑に落ちない。docというモジュールのToHTMLメソッドで生成できるっぽい気もするが、あんまり使っている人がいない。

「godocがサーブするHTMLをwgetcurlでダウンロードする」という作戦の人達もいて、今の所それが最も現実的だと判断した。Makefileに以下のようなことを書いておいて、make apidocを実行すると、API文書のHTMLファイルがディレクトapi-docの中に作られるようにした。JavaScriptの無効化やCSSの修正をするとともに、「-」で始まる行をリストの項目として扱うようにもしている。

apidoc :
  rm -rf api-doc
  godoc -http "localhost:8080" -play & sleep 1
  mkdir api-doc
  curl -s "http://localhost:8080/lib/godoc/style.css" > api-doc/style.css
  echo '#topbar { display: none; }' >> api-doc/style.css
  echo '#short-nav, #pkg-subdirectories, .pkg-dir { display: none; }' >> api-doc/style.css
  echo '.list { display: list-item; list-style: circle outside; }' >> api-doc/style.css
  echo '.list { margin-left: 4.5ex; }' >> api-doc/style.css
  curl -s "http://localhost:8080/pkg/github.com/estraier/tkrzw-go/" |\
    grep -v '^<script.*</script>$$' |\
    sed -e 's/\/[a-z\/]*style.css/style.css/' \
      -e 's/\/pkg\/builtin\/#/#/' \
      -e 's/^- \(.*\)/<div class="list">\1<\/div>/' > api-doc/index.html
  killall godoc

Go言語はC言語の文法をちょっと変えて並列処理がしやすいように計らった言語という印象だ。ゴルーチンで並列処理が簡単にできるようになり、チャンネルによって並列環境での同期処理が簡単にできるようになり、ガベージコレクションによってリソース管理が簡単にできるようになっている。それ以外は、ポインタがあったり、構造体やその他のオブジェクトのサイズが取得できたりなど、めちゃC言語っぽい。C言語のノリの低水準を保ちつつ、メモリ関連のバグが起きないように各種の配慮がなされている。型チェックがやたら厳密なのが特徴的で、intとint32の間で暗黙のキャストがなされないくらい。当然、[]byteとstringは異なる型なので、相互に明示的な変換が必要だ。DBMは任意のバイト列の連想配列を格納する機能を提供するが、実際には文字列を格納するユースケースが多い。よって、PythonRubyのように文字列でもバイト列でも扱えるAPIを提供したいが、そのためには空インターフェイスとインフレクションを併用すればよい。以下のようなサンプルコードになるはずだ。

// データベースを作って開き、勝手に閉じるようにする
dbm := tkrzw.NewDBM()
dbm.Open("casket.tkh", true, "truncate=true")
defer dbm.Close()

// 文字列のキーと文字列の値を格納。暗黙的にバイト列に変換される。
dbm.Set("japan", "tokyo")

// バイト列をそのまま格納
dbm.Set([]byte("china"), []byte("beijing"))

// 文字列で検索してバイト列を取得。文字列に変換してから印字。
raw_value, status := dbm.Get("japan")
if status.IsOK() {
  fmt.Println(string(raw_value))
}

// バイト列で検索して文字列を取得。そのまま印字。
str_value, status := dbm.GetStr("china")
if status.IsOK() {
  fmt.Println(str_value)
}

APIの細かい仕様はまさに今実装しながら詰めているところだが、おそらく近日中に完了して公開できると思う。今気づいたが、ローカル変数をスネークケースで書く癖がついていた。全部キャメルケースに直さにゃならんのか。