CLOSE

Goの構造体とタグを極める(全2記事)

「Goらしさ」を追求するなら“タグ”を使え ライブラリを使って開発効率を上げる

Go Conferenceは半年に1回行われるプログラミング言語Goに関するカンファレンスです。渋川氏は、タグを使ったGo開発について発表しました。全2回。後半は、実際にタグを使ってライブラリを実装する方法を紹介しました。前回はこちら。

タグを使うライブラリの実装する

渋川よしき氏(以下、渋川):タグを使うライブラリを実装するとGoのパワーを引き出せると思うので、ぜひみなさんにもやってもらいたいんですが、ここからはタグを使うライブラリを実際に実装してみます。

細かくするとけっこう退屈なセッションになってしまうので、実装のコードは飛ばしめに説明してきたんですが、実装となるとつまずきポイントもけっこうあると思うので、一応ひととおり動くサンプルコードを用意しました。実装するときはぜひ参考にしてもらえればと思います。

うちの会社が作った、「future-architect」というorganizationがGitHub上にあって、そこにオープンソースのコードなども出しているんですが、一応そこに今回のサンプルコードもアップしています。

さっき紹介した巨大なswitch文になる例ですね。これがそのコードです。型が文字列型のときに、例えば整数型やboolean型が来たらfalseやtrueの文字列にして入れるとか、格納先がintのときに文字列だったらParseIntしてからやるとか。こんな感じのコードが大量にできます。

これが1セットなんですが、格納先がポインタだったら、このぐらいのコードにはなります。このあたりの500行ぐらいの関数を毎回作ると大変だと思うので、ぜひこのままコピーして使ってもらえればなと思っています。

だいぶがんばって書いたんですが、このコードも完全ではなくて、slice型のときなどいろいろとやらなきゃいけないところをちょっと端折っています。実際未知の型が来たときには、panicを出すようになっていて「Pull Request出してくださいね」みたいなメッセージを出すようにしています。

これを完全に網羅するのはかなり大変なんですが、「みんなの叡智でそういう関数が1個できれば、みんなで使い回せていいかな」みたいな感じで書いています。本当にこれは実装していてとても退屈なコードですね。

実際このライブラリを使っている例で、同じインキュベーターなプロジェクトで作り始めています。日本だとO/Rマッパー的なところでよく出てくるのが2 Way SQLです。

これは海外では一般的な言葉ではないんですが、制御構文がコメントとして入っているSQL文をテンプレートとして使えるというものです。コメントなので制御文を無視してそのまま実行もできるし、その制御文に応じて、例えばSELECT文のフィールドをちょっと増やしたり削ったりというのを実行時のパラメータで変化させたり、ループで回してbatch insert みたいにする感じのライブラリです。

Goの2 Way SQLのライブラリがなかったので「社内的になにか作りたいね」という話で作り始めています。まだ技術ブログでの紹介はしていないんですが、そのうち公開もされる予定です。

あと、これはさっきの僕の作ったリポジトリの中にサンプルで入れているもので、HTTPリクエストを構造体にマップするものです。タグの文字列で、bodyのtitle-fieldという項目をここに入れるとかヘッダーを入れるなどを書いておくと、HTTPリクエストをマップしてくれます。

こういう実装をするときは、タグは文字列で取得できるので、正規表現やstringsパッケージを使います。例えばこの「:」で区切って「前の部分がタグの種別で、後ろがヘッダーの名前ですよ」みたいに処理をする必要があります。

実際にロジックを回すときは、この「:」の前の文字列を見て、それがswitch文になって、reqオブジェクトのメソッドを取ってきたり、ヘッダーから取ってきたり、クッキーから取ってきたりする感じです。

Goだとchiとかけっこうシンプルなフレームワークはあるんですが、OpenAPIを使うとけっこうすぐに大げさになっちゃうので、入出力の部分などをもっとシンプルに書きたいなという感じで作り始めています。

一応bodyはContent-Typeを見て処理方法を変えるようになっていて、Content-Typeがapplication/jsonであれば、JSONパースしてからJSONのフィールドを取ってきます。それでmultipartであればmultipart/formとしてパースしてから取ってくる感じです。

JSON APIをけっこう気軽にみなさん作るんですが、JSON APIってcurlコマンドで叩きにくくて、テストがけっこう面倒くさいので、サーバー側でそのあたりを吸収するライブラリがあったらいいんじゃないかなと思って、そういう実装にしています。

パフォーマンス向上のための改善点

次がBinary Pattern Matchですね。バイト単位やビット単位で並んでいる情報を構造体にマップできたら便利かなと思ってやりました。参考にしたネタは、Erlangのバイナリパターンマッチですね。

値をただフィールドにアサインするだけじゃなくて、特定の値にマッチする場合だけ成功して、そうじゃないときはエラーを返すという実装にしています。

この場合も、タグの分解は正規表現かstringsパッケージを使います。ストリームから読み込んできたデータを変数にするのは、中で愚直にビット演算を駆使してやっています。これも先ほどの僕が作ったリポジトリの中にコードとして入れています。

TCP/IPを使ったプロトコルの実装に使えたらいいなとちょっとやり始めています。いろいろとライブラリを作ってみたんですが、パフォーマンス向上を工夫してやらないと、パフォーマンスが悪くなりがちだなというところを、いくつか列挙しました。

例えばJSON decodeのたびに、その構造体を毎回パースしているとパフォーマンス上やっぱりよくないと思うので、探索結果はキャッシュして使い回せるといいかなと思っています。ここは何回かやっているうちにすぐなんかもったいないなと思ったので、すでに実装しています。

また、Binary Pattern Matchの場合「ストリームから読み込んだときにこうする」みたいに機械的に処理するインタプリタを実装するといいかなと、あとから思いました。

例えばこの場合だと、フィールドは固定値なので、このバイト数分は先に読み込んでしまって、その何バイト目から何バイト目を取り出してくるとか、データを取り出した上で0x0100でマスクして入れるとか、そのあたりをインタプリタ的な命令にしてforループで回す処理をしたほうがパフォーマンス上はいいかなと思いました。

今は毎回タグのフィールドを見て、「これが4だから4バイト取ってくる。次のフィールドはまた4だから4バイト取ってくる」と、毎回ここのタグの解釈をしているんですが、タグの解釈は先に全部やってしまってキャッシュするといいかなと思っています。

ほかにも、さっきのField(i)で取ってきたときってフィールドの定義順で返ってくるんですが、例えばXMLのSAXパーサだと、データが入っている順番と構造体のフィールドの順番が必ずしも一致しません。優先するのは基本的にデータ側だと思うので、そのデータ側の順序に合わせてフィールドの順番を調整してあげたらといいなと思います。

さっきのコード例だとfor文で回してるんですが、名前がキーになっているマップを作ってあげて「どの順番で来てもフィールドを返せますよ」みたいにするほうが、データに合わせて実行時のパフォーマンスは良くなるんじゃないかなと思います。

GoはXMLパフォーマンスが悪いって毎回話題になるんですが、そのあたりももしかしたら自分でタグ処理ライブラリを作れば改善できるんじゃないかなという感じですね。

2通りあるコード生成方法 それぞれのメリット・デメリット

次はコードジェネレーションです。今までは実行時の処理の話しかしなかったんですが、タグを使う処理というのは、コード生成みたいなところでもあります。

この場合の方法は2つあって、実際の世の中に出ているライブラリでもだいたい2種類あります。1つは、ユーザーがプログラムを作って実行すると、リフレクションでデータを取ってきてコードを生成するというもの。もう1つが、go/parserを使って、静的解析を行って静的にタグ情報を取得してコードを生成するというもの。この2通りがあります。

静的なコード解析だとparserパッケージを使います。フォルダを指定して、フォルダの中の「.go」ファイルをWalkで取ってきて、それをparserに渡して取ってきます。

これでパースした上でastのInspectでStructTypeを取ってきます。Fields.Listを取ってきたそのタグ情報を処理するという感じです。

タグのパースは、reflectパッケージですね。動的に処理するときに使うのと同じreflectパッケージを使って無理やり値を取り出しています。このあたりは文字列処理なので最終的には好きなようにやればいいかなと思います。

コード生成方式のメリット・デメリットです、リフレクションは、実行時のタグ処理のコードとロジックを共有できるところがいいところかなと思います。ただ、僕もいろいろとライブラリを探していて、コード生成するためにコードを書かなきゃいけないというのを見ると「うっ……」ってなるので、コード生成のためにコードを書かなきゃいけないのは、ユーザビリティ上やっぱりよくないかなと思います。

静的解析は、ユーザーとしてアプリケーションコードを作らなくていいので使い勝手はいいんですが、実行時のタグの処理のコードが流用できません。特に型周りですね。文字列情報としてしか型が取れないので、reflect typeが取れません。そのあたりをうまく相互乗り入れできたらいいなと思うんですが、ちょっとまだ解決できていないですね。

このあたりも先ほどのライブラリのフォルダの中のstaticscanというサブパッケージの中で実装しているので、ぜひ参考にしてください。

型アサーションの減少を期待できるGenerics

次がGenericsとreflectです。go2goで試されている人が多いと思うんですが、今まで柔軟性を上げるために「インターフェイスを使って、型情報を切り捨てて、型アサーションをがんばってあとからつける」みたいにしていたところを、Genericsで改善できます。

最大の効能は型アサーションが減るところです。身も蓋もないところかなと思うんですが、僕の思っているGenericsの効能ですね。

ただ、Genericsってやっぱりメソッドの有無のチェックでしかなくて、フィールドに対するサポートはないので、フィールド情報を取り出すときにはreflectを使う必要があります。Genericsが入ってもreflectを勉強したことは無駄になりません。

ただ、Genericsが入ることで今後APIのかたちが変わってくるかなとちょっと思っています。例えばコンパイル時に、デコードやエンコードだと構造体のインスタンスのポインタを入れてほしいみたいなことがあります。API上はインターフェイスなので、実行時しかエラーがわからないのが、「*T」と書くとポインタ型であることを強制できるので、ユーザーの使い勝手はちょっと上がるんじゃないかなと思います。

もう1つ、デコード時に構造体のインスタンスを内部で作れるのがあるかなと思います。今までは、Decodeにインスタンスを外から作ったものを渡していましたが、Genericsが入ればDecodeとAssignの2つがAPIを提供できます。

Assignは今までのデコードと同じで、Decodeは「呼び出し時に型パラメータで構造体の型を渡すと、内部でインスタンスを作って返す」みたいな感じです。使う側からは、1行にまとめられるようになるんじゃないかなと期待しています。

Goらしさを維持しながら開発効率を上げるにはタグの使用が効果的

ということで、まとめです。

Goらしさを維持したまま開発効率を上げる上では、タグを使うのは、すごく大事かなと思います。僕も含めて、今まではただタグのライブラリを使うだけだった人たちも、今後はライブラリをガンガン作って、さらにGoの魅力をアップできたらいいんじゃないかなと思っています。

あとは、静的解析や動的リフレクションなど、このあたりの高度な実装もどんどん駆使して、もっといいものを作っていければいいなと思っています。Genericsが入ってもreflectとは縁が切れないので、そこは残念ですが、がんばりましょうという感じですね。

このライブラリは、とりあえずコンパイルの通る動くサンプルコードとして公開しているので、APIの安定性はあんまり期待しないでください。

このあたりの話も含めて、技術ブログには今後もどんどんGoのネタを上げていきたいと思っています。採用系のブログもあるので、興味のある方はぜひチェックしてください。

以上です。どうもありがとうございました。

司会者:渋川さん、セッションありがとうございました。タグを使えるようになると「Goやってるな!」という気持ちになるので、渋川さんの記事などを見て私もGoのライブラリやタグ使うものを書いてみようかなと思いました。ありがとうございました。

渋川:ありがとうございました。

続きを読むには会員登録
(無料)が必要です。

会員登録していただくと、すべての記事が制限なく閲覧でき、
著者フォローや記事の保存機能など、便利な機能がご利用いただけます。

無料会員登録

会員の方はこちら

この記事のスピーカー

同じログの記事

コミュニティ情報

Brand Topics

Brand Topics

  • 孫正義氏が「ノーベル賞もの」と評価する最新の生成AIとは “考える力”を初めて身につけた、博士号レベルを超えるAIの能力

人気の記事

新着イベント

ログミーBusinessに
記事掲載しませんか?

イベント・インタビュー・対談 etc.

“編集しない編集”で、
スピーカーの「意図をそのまま」お届け!