Goの特徴はタグによる関数の呼び出しのシンプル化

渋川よしき氏:よろしくお願いします。それでは「Goの構造体とタグを極める」を発表します。

渋川と申します。株式会社本田技術研究所や株式会社ディー・エヌ・エーなどで働いて、今はフューチャー株式会社で仕事をしています。妻と子どもがいます。いろいろと本を書いたりしています。週末はインラインスケートを子どもたちとやっています。

これが今まで書いてきたGoの本です。『Real World HTTP』は韓国語バージョンも買えます。もう1ヶ国語を今翻訳中です。

当社では技術ブログにけっこう力を入れていて、最近勉強会で発表すると「『Go』で検索すると出てくる会社の人だ」と言われたりもします。ぜひいろいろチェックしてもらえるといいかなと思います。最近はGo 1.16のリリースに合わせて、「1.16でこういう更新がありました」みたいな内容を集中的に連載しています。

どんな技術ネタを検索しても必ず15年くらい前のIBMの「IBM DeveloperWorks」が引っかかるのがちょっといいなと思っていたので、いろいろなネタを掲載している当社のテックブログもIBMみたいにしていきたいなと個人的には思っています。

ということで、セッション発表に進みます。

アジェンダです。まず、Goの文化とタグについて紹介します。次に、タグをどうやって処理するかを説明していきます。そのあとに実装例の紹介をして、あとはタグの活用方法としてCode Generationの説明も軽くしようと思っています。Genericsの話もちょっと足したので、まとめで発表します。

まずGoの文化とタグですね。

ふだんからGoを仕事で使っている方も多いと思うんですが、巨大なシステムを開発するときになにも作戦なしでいくのはやはり厳しくて、なにかしらの方法を決めて開発していると思います。

多くのプログラミング言語は、そういう巨大なシステムをメンテナンス可能にしつつ、開発するためのメカニズムをいろいろと模索しながら提供してきました。オブジェクト指向もそうですよね。最近だと、状態を持たないとか、冪等性とか、いろいろな感じで状態を管理しやすくすることで、大きなコードになってもわかりやすくしたり、バグを出にくくしたりしています。

エンタープライズだと、今までJavaがけっこう使われてきているんですが、Javaの場合は、Dependency Injectionだったり、リフレクションだったりを駆使した巨大なフレームワークを作って、フレームワークが処理の流れを完全に制御して、一部分のロジックをプログラマが開発するみたいな流れです。

Goはだいぶやり方が違っていて、タグ付きの構造体みたいなものを使って、短いコード片の中にたくさんの宣言を入れて、裏でいろいろなロジックを走らせるのが特徴かなと思います。

図で説明したのがこれです。エンタープライズのアプリケーションで、けっこうたくさんのロジックを書かなきゃいけないときに、Goの場合はそのアプリケーションコードからそれぞれの複雑なロジックを呼び出します。アプリケーション固有のロジックは、呼び出し側にシンプルに集めるところがGoらしいコードかなと思います。

Goをやるときには「Go流でやりましょう」みたいにみなさんけっこう言うんですが、ほかの言語から取り入れたフレームワークはあまり流行っていないのかなと思います。

Javaなどよくあるフレームワークだと、フレームワークが全体を制御して、ホットスポットと呼ばれるアプリケーションのコードだけを部分的に差し込んでいく構造になっています。

僕の同僚がGoを評したコメントがおもしろくて、Javaユーザーから見ると、「Goというのは、なんかmain functionがあるぞ」と思うそうです。Javaにもあるんですが、main functionや起動からの流れは完全に隠されているので、それに比べるとGoはmainから順番に処理を追っかけていけるので、そこがわかりやすさにつながっていて、人気にもつながっているのかなと思います。

Javaのフレームワークでアプリケーションを作る場合、クラスはいっぱい作るんですが、結局誰がインスタンス作っているのかわからん、みたいなことがあります。

Goではメインの処理の中で、短い関数呼び出しで多くのロジックを駆動するんですが、そのときに使われているのがタグです。

タグは構造体の中に入れていくんですが、「このフィールドはこういう処理を裏で実行しますよ」みたいな宣言を書くことで、関数呼び出しのところだけ見るとシンプルだけれども、裏でいっぱいのことをやっていますよ、みたいなことができます。

例えばあるWebサービスで、リクエストが来ました。そしたら、まずそのリクエストを構造体にマッピングします。そのあとに、その構造体のままいくか、コピーを作るか選択して、今度はその構造体に対してValidationをします。Validationが通ったら今度はDB Accessです。DB Accessも構造体をsqlxなどのライブラリに渡して、SQL文を生成して実行して、返ってきた行オブジェクトもO/Rマッピングします。

これも構造体のタグを使って構造体にマップして、最後それをまたencoding/jsonなどのライブラリでJSONにシリアライズする感じで、けっこう複雑な、それでいて繰り返し使われるロジックは、このタグのマッピングでけっこうシンプルに使われています。

Go開発の生産性を支える多種多様なライブラリ

ざっと僕が使ったことがある、タグを使うライブラリをリストアップしてみました。例えば環境変数を構造体にマッピングしますとか、コマンドライン引数を構造体にマッピングしますとかですね。ほかにもJSONやTOMLなど、シリアライズやデシリアライズのライブラリもありますし、バリデーションもデータベースアクセスもタグを使えます。gocloud.devはDynamoDBやFirestoreへのマッピングを提供しますし、MongoDBや構造体間のコピーなど、本当に多種多様のタグを使うライブラリがあります。

ライブラリなしでGoのコードを書くと、コードの実装量がすごく増えるので、Goの開発の生産性を支えているのは、このあたりのタグを処理するライブラリかなと思います。

このタグの処理はすごく活用されているとは思うんですが、自分で実装する方法って、ドキュメントなどを探しても世の中あまりないなと実感したので、今回自分でいろいろと調べた内容を発表することにしました。

ライブラリなどを世間に公開するときは、やっぱりGoらしいコードほどみなさんに喜んで使ってもらえると思うんですが、タグを自分で処理する方法がわかれば、そのGoらしさを維持しつつ、Goのパワーをさらに引き出しやすくなるんじゃないかなと思いました。

Goのタグ処理ですが、タグ処理専用のライブラリは存在していなくて、比較的ふだんの業務で使わないようなreflectパッケージやgo/parserパッケージを使うことになります。reflectパッケージは、たまにデバッグ出力などで使う人もいるかもしれませんが、タグ処理だとまた独特な使い方をするので、そのあたりを本セッションの中では紹介していきたいと思っています。

タグ処理の3ステップ

どのようにタグを処理していくかですね。まずタグ処理の基本なんですが、プロセスを3つに分けて説明していきます。

まず構造体の型を取得します。それぞれの構造体にはフィールドがあって、NumFieldでフィールドの数が取れます。t.Field()というメソッドを呼ぶと、そのフィールドの状態のインスタンスが取れます。フィールドの構造体には、Tagというメンバーがいて、それに対してGet()というメソッドを呼ぶとタグの情報が取得できます。

基本的にはスライドの下に書いたように、タグを取ってきてNumField分でForループを回して情報を取ってくれば、Tagの一覧は簡単に出せます。ただ、実用的なコードだと、構造体のフィールドにさらに構造体がいる場合があって、それをネストして処理するのがけっこう必要になります。

これが再帰をするバージョンですね。構造体の型の情報を取ってきて、NumField分でForループを回すんですが、Anonymousは埋め込みですね。embeddingのときと、あとはフィールドの型がStructのときにはさらに再帰で呼び出します。

次は2ステップ目です。さっきは構造体の型を使っていたんですが、今度は型ではなくて、値からフィールドの値を取り出します。タグ処理の場合は型の処理と値の処理、インスタンスからデータを取ってくるという2種類あるので、パッとコードを見るとけっこうわかりにくいところですね。

フィールドの実際の値を取ってきて、タグにその値をどう加工するとか、どう処理するとかの文字列が書いてあるので、それを使ってその値に対していろいろと処理を加えるのがステップ2ですね。

フィールドのValue型にはInterface()というメソッドがあって、いつものAny型的に使われているinterface型が取れるので、あとは型アサーションを使って、いつもどおりに変数に入れられます。

これがそれですね。さっきはreflect.ValueOfのあとにType()というメソッドを呼んでいたんですが、これはType()を呼ばないでその実体のElem()メソッドを呼んで値を取ってきてます。それから、値側のNumFieldと値側のフィールドから値を取ってきて、いろいろと処理を回します。

次はタグ処理の基本のステップ3です。さっきはフィールドから値を取り出すというところ説明したんですが、タグ処理の場合だと逆に構造体のインスタンスに値を入れるということがよく行われます。

例えばJSONの文字列から構造体のフィールドに入れていくとか、データベースの行オブジェクトから構造体にしていくとかですね。その場合、値のSet()やSetSring()などのreflect.Value型には値を更新するみたいなメソッドがあるので、そのあたりを使って更新していきます。

ただ、これを実際やったことがある人もいると思うんですが、入力の値の型とか格納先の値の型とかあって、しかもポインタ型がいたりもして、いろいろな組み合わせがあるので、実際にやってみると超巨大なswitch文ができあがります。

ここまで3つの処理1・2・3を紹介したんですが、どのタグ処理のコードを書く場合でも、この3つの組み合わせで実用的なライブラリを作れるかなと思います。

構造体があったときに、その値に対して処理するのが2番と3番ですね。値を取り出して値を割り当てます。構造体のインスタンスの型情報を取ってきてメソッドを呼ぶのがタグ情報の取得です。

タグ処理4パターン「Encode」「Decode」「Piping」「Zipping」

タグの処理のパターンは4通りあると思います。この中には世間にあるライブラリを使っていないパターンもあるんですが、きっとこういう処理もあるんだろうなというところで4通りのパターンにしています。これまでいくつかタグを使うライブラリを紹介してきたんですが、すべてこの4つのパターンに含まれます。

まずよくあるEncodeですね。json.Encode()みたいなものです。これはまずタグを処理して、タグの情報を取ってきます。その情報をもとにインスタンスから値を取り出します。取り出した値はmapにとりあえず入れてみたり、文字列化してみたり、取り出してきた値をもとにバリデーションのロジックを実行したりします。

次がDecodeですね。これもjson.Decode()と同じなのでDecodeと名付けています。これは構造体のインスタンスに値を設定します。例えばjson.Encode()はなにもない場合、フィールド名をもとにしてJSONのキーがオブジェクトのキーと一致するとみなして値を取ってきます。JSONのキーとフィールドの名前が違うときもマッチングさせて入れたりというところで、タグの情報を使っています。

出すときは無視するフィールドもあったりしますが、Decodeは名前のマッチングぐらいですかね。このタグの文字列を分析した結果を構造体に割り当てます。

次はPipingと名付けてますが、さっきの値を取り出す処理と割り当てる処理を一緒にします。同時に行う感じですね。ライブラリとして見たことがあるのは構造体間のコピーなんですが、同一インスタンスで値の正規化をするとかもきっとあると思いますし、構造体に値がなくてゼロ値だったときにデフォルト値を設定するライブラリもありなんじゃないかなと思っています。

これ(Zipping)は実装しているライブラリはあんまりなさそうかな。構造体同士の比較をする感じで考えました。テストコードを書いていて、構造体をインスタンスがリターンで返ってくるときに、「実はこのフィールドを無視してテストコードで比較したいんだけど」みたいなときが絶対あると思います。そういうところのライブラリを作るんだったら、それぞれのインスタンスから値を取り出してmapかなにかに入れておいて、そのmapを比較するみたいなライブラリがありなんじゃないかなと思います。

(次回へつづく)