Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】『改訂2版 みんなのGo言語』:Golangの現場のテクニックが学べる本

Golangの現場の忍者(うそ)から学んでみよう

 ちょうど入門していたので入門書の次に1冊読んでみた本です。著者陣は面白法人カヤック、メルカリ、VOYAGE GROUPなどなど各社より、Goを実践している重鎮の方々。2016年に初版が出た後、表紙も忍者ゴーファーくんたちに変わり内容も全面改訂された改訂2版が2019年に出ました。以下、読書記録と感想です。

 前書きでは本書の作者さんたちからは、Goのメリットとしてはパフォーマンス、メモリ管理からの解放、コンパイル速度の速さ、コード整形、言語仕様のシンプルさ、シングルバイナリの手軽さが上げられています。
「人類にメモリ管理は難しすぎる」と書いてあったりして、やはり苦労する局面はあるのだろうなあと思います。ブラウザのChromeFirefoxの歴代の重大バグのほとんどはもうメモリ回りらしいですね。

Go忍者の技を学ぼう

第1章 Goによるチーム開発のはじめ方とコードを書く上での心得

最初はインストール回りから。

  • 2019年の本なので外部パッケージはgo install ではなくまだgo getになっています。
  • インストール対象の書き方は{リポジトリのFQDN}/{リポジトリのパス}
  • GoのREPLの代表格 gore を入れると対話型で便利。
  • 階層構造でソースを取得、管理できる ghq が便利
  • リポジトリ間の移動には pecoが便利

  • エディタではvim/emacsなどテキストエディタ、あるいはGoLandかVSCodeの3系統。本書の作者さんたちはvimなどターミナル推し

  • Go用の巨大IDEではなく小さなツールを多数用意、小さなものを組み合わせて大きな機能を実現するというUnix思想の流れを受け継いでいる。
  • 支援用ツールは go get(install) golang.org/x/tools/cmd/... のように、...でサブディレクトリすべてが取れる。
  • go fmt でフォーマットは統一。FreeBSDコミュニティで生まれた「自転車置き場の理論(=些細なことで議論が紛糾する)を避けている。
  • goimports が、import文の自動挿入と削除
  • lintツールには標準のgo vet のほかにgolintもある
  • 標準のgo docでドキュメント閲覧。名前がややこしいがほかにgodocというツールで、httpサーバーを動かしてその上で見ることができる。
  • gorename 変数や関数のリネーム
  • guru 静的解析
  • gopls 公式の言語サーバープロトコルで今はデフォルト。
  • gocode コード補完エンジン
  • godef 定義ジャンプ
  • gotags タグ生成

VSCodeの環境構築で一緒に入るものもありますが、基本的なツールでもいろいろあるのだなあという感じ。

  • ディレクトリ構成の代表的な例。
    • ルートがもうパッケージでmyproj.go などを配置
    • サブパッケージはルートの下に掘る
    • cmd/の下に実行ファイル
    • ルートに置いたMakefileでビルド定義やタスクランナー
    • testdata_ で始まるディレクトリはパッケージと認識されないルールがある。
  • ファイル分割は、type Hoge の定義+それに対するメソッドで1ファイルにするとよい。
  • クラス定義の階層と名前空間が対応している他の言語の感覚でパッケージ分割するのはNGで、パッケージ数はそれほど多くならないはず。
  • 「他のプロジェクトから使えるかどうか」がパッケージ判断の基準。
  • サブパッケージのimportは絶対パスを推奨。
  • Go Modulesの使い方

 ディレクトリの構成の話はとてもありがたいです。他の言語だと/srcなどいったん掘ってそこにコードを置くことが多い(気がする)ので、Go言語はこのへん若干独特ですね。  そしてお役立ちなのが「1.4 Goらしいコードを書く」の節。

go.shibu.jp

  • 慣れてきたらEffective Goを読むのがおすすめ。
  • 適当にpanicを使うのはNGで、関数の末尾の戻り値でerrorを返す。呼び出し元はエラーチェックをする。Goでは愚直でも素朴に描く。
  • 正規表現regexpパッケージは最悪ケースを想定しているのでパフォーマンスが悪い。文字列操作がstringsパッケージで間に合うならそちらを。
  • どうしても正規表現を使う場合は動的生成でなく初期化時に行う。
  • map[string]stringなどを安易に多用せず、構造体を使ったほうがよい。
  • reflectパッケージを使ったリフレクションは基本避ける。黒魔術に手を出さない。
  • Goは継承がない。巨大なstructは作らず、小さなものを組み合わせる
  • 並行処理も使いすぎると可読性が落ちたり見つけずらいバグが出てくる。基本は直列処理でコードを書き、時間がかかりそうな箇所で並列処理を活用。
  • 標準パッケージのコードを読むと学びになる。
  • CIでgo vetgolintの検査も一緒に行うと良い。
  • go build の際に引数でバイナリに情報を埋め込んだり、タグを指定してdebug/prodを切り替えたりするテクもある。
  • 常駐プロセスはモニタリングするとよい。

 正規表現のパフォーマンスが悪いなんて話はへぇーと思いました。かなり堅牢にしっかり作られているように見えるGo言語でもこういう特徴あるんですねえ。まさに現場からのお役立ち情報です。

Go忍者の技を学ぼう

第2章 マルチプラットフォームで動作する社内ツールのつくり方

 社内には様々なOSや環境があり、ランタイムの別途インストールが難しい場合もある。こんな時こそバイナリを配布しさえすればよいGo言語の出番だ...ということで、社内ツールを題材に様々なテクニックを紹介する章。

  • Unix系OS/Windowsでのパス操作の違い問題は頻出。Goではpath/filepathパッケージで物理パス回りを吸収できる。httpなどの論理パス操作にはpathパッケージなので間違わないこと。
  • deferを活用。呼び出した順番(≒上から下に書いた順番)と逆順に実行される。関数を呼んでオブジェクト生成の次の行で変数errのチェック、その直後でdeferで後処理(ディレクトリ削除など)をするように習慣づけると、たいていGoでは問題ないコード構成になる。
  • Windowsでコマンド引数からとった値は文字コードCP932と想定、いったんUTF-8にデコードしてからGoプログラム内で扱うなど、UTF-8を積極的に使う。

あーこのへん確かに問題が出そうなところだな~と思いながら読みました。Javatry-with-resourceを使わずリソースが破棄できないまずいtry-catchのコード例も載っているのですが、Go言語のdeferはこういうケースを考えて関数内に複数書けるようになっているのか...と思ったり。

 続いてテキストベースのコンソールアプリを作る話へ。TUIというキーワードが出てきた何かと思ったらText-based User Interfaceの略でした。

  • LinuxでもWindowsでも動くマルチプラットフォームアプリにはtermboxが便利。これを使っているGo製アプリも多い。
  • Windowsで色付きのログ出力をうまくやるにはgo-colorable
  • コード内でOS固有処理の分岐をするには標準ライブラリのruntime.GOOSで判別できる。
  • GoにはBuild Containsという機能がある。ディレクトリごと> go buildする際、xx_windows.goxx_linux.goなどのファイル名で実は判別してくれる。それぞれのファイルの中に固有の初期処理を描くなど。
  • もうひとつはファイル内に //+build でコメントを書く方法もある。
  • Goのコード内からC言語を呼び出すにはcgoというコマンドを使う。
  • 拡張子.pcのファイルが付属しているライブラリ類では、pkg-configコマンドでオプションを知ることができる。
  • Go単体では実はプロセスをデーモン化できない。他のアプリの力を借りるほうが早い。Linuxで作者さんのおすすめはupstartWindowsではnssmというアプリがおすすめ。
  • なんでもGo単体で解決しようとする必要はない。アプリはそれがやろうとしていることだけをうまく実装した方が良い結果になる

 このへんはUNIX思想をしっかり受け継いでいるなあと思いました。

  • Goの実行ファイルはシングルバイナリだが、テンプレートや画像ファイルなどを入れたい場合はアセットツールを使う。
    • statik: /publicindex.htmlなどを入れてバイナリに埋め込める。コード内からでもこのファイルにアクセスできる。
    • Goでは //go:generate というマジックコメントでコマンド実行ができるので、そこにstatikと書くとビルド時に静的ファイルからstatik.goが生成、一緒にビルドされて実行。
    • packr: 同じようにコード内からファイルにアクセス可能。他にファイル一覧を取得したりもできる。
  • なお本書には載っていませんが、Go1.16から標準のembedパッケージでstatikと同じことができるそうです。
  • Windowsアプリを作る際はビルド時の-Hオプションでコンソールが出ないようにしたり、アイコンのリソースファイルも含められる。

  • Goのアプリで設定ファイルに固定はないが、JSONYAMLが主。

    • INIファイルはWindows専用になってしまう。
    • JSONはコメントが書けないがRFCで仕様が明記。標準パッケージで使える。
    • YAMLもコメントが書ける、学習コスト必要。
    • TOMLという新しいフォーマットもある。
  • 置き場所はLinux$HOME/.config/配下、Windows%APPDATA%\{アプリ名}\config.json
  • ホームディレクトリ取得にはos/userパッケージは使わない方がよい。環境変数から現在パスを取得して設定ファイルを読むのがおすすめ。
  • json.Marshalよりjson.MarshalIndent を使うとJSON文字列がきれいになる。

 あーこうやって作るのか~と分かる実用的な章でした。設定ファイルはやはりJSONが主流なんですね。Webアプリはアセットツールを使ってexeの中に入れちゃうんですね。

Go忍者の技を学ぼう

第3章 実用的なアプリケーションを作るために

 こちらも実際の使用、運用に耐える本格アプリでのTipsを紹介する実用的な章。作者さんがご自分で作ってGitHubで公開しているというプル型デプロイツールStretcher、FluentDにログを送信するfluent-agent-hydraがさらっと出てきて、つよエンジニア!という感じです。これらのOSSを題材に紹介していきます。

github.com github.com

  • 実用的なアプリの要素:持っている機能を容易に調べられる、良パフォーマンス、多様な入出力(HTTP/ファイル/標準入力など)、人間に読みやすいログ出力、想定外の場合に安全に停止できる

  • コードにビルド番号を埋め込み、実行時の引数で-vなどが来たらflagパッケージで取得して表示すればよい。go build時に -Idflag を指定するとビルド時に引数で任意の変数設定もできる。

  • 最新バージョンを識別する方法もいくつかある。

    • GitHubのタグ
    • HTMLのメタタグで meta name="go-latest"として情報を埋め込み、Web上に置いておく。プログラムからURLにアクセスして中を取得。
    • 同じくWeb上でJSONを返すようにして中に情報を埋め込み、プログラムからアクセスして中を取得。
  • Goはio.ReaderWriterインターフェースで入出力を順次処理。自動でバッファリングはされないので、bufioを使うとバッファサイズを指定したりして活用できる。

  • ライブラリのgo-isattyを使うと出力先が端末か判定できるので、端末ならバッファしないなどの分岐もできる。

他の動的言語での繰り返し処理と比較して、Linuxでstraceコマンドを使ったシステムコール発行回数比較の実例まで載っており実践的です。おおー低レイヤーの世界に近づいている...という気持ちになります。

  • 複数ソースからコンテンツを取得する際の実装例。
    • 取得元はファイル、HTTP(S)、AWSのS3、GCP
    • Go言語のio.ReadCloserインターフェースは、Read([]byte) (int, error)Close() errorメソッドの実装を強制する
    • 題材になっているOSSのStretcherの処理では、スキーマを識別してswitch文でそれぞれのソースごとに内部関数に分岐。
      • ファイルだったら普通に標準のos.Openを呼ぶだけ
      • HTTP(S)だったら標準のnet/httpを使う。戻り値のhttp.Response#BodyReadCloserを持っている
      • Amazon S3だったらGo言語用のSDKを使って取得。これも戻り値がReadCloserになっている...
    • 内部関数に渡す引数、戻り値を同じインターフェースに統一することで、今後の拡張性も備えられる。

Goのインターフェースはこうやって使うのか! とこのへんのコード例が自分的にはしっくり腑に落ちました。こういうところはオブジェクト指向言語のインターフェースと大体は同じ使い方なんですね。

  • 複数の出力先に一度に書き込むにはio.MultiWriterがある。引数を複数とって標準出力とログの両方に書き込んだり。
  • 議事乱数生成にはmath/randがあるが、同じ引数を与えると毎回同じ乱数になる。現在時刻を与えるより良質の暗号を生成するには、crypto/randを組み合わせるとGood。
  • ファイルサイズなどを人間が見やすく表示してくれるgo-humanizeというライブラリがある。
  • アプリ内部から外部コマンドを実行するには標準のos/execパッケージ。標準入力をコマンドに与えて結果をプログラム内で取得...などの実装にはそれぞれをゴルーチンで動かす。
  • UNIX系のシェルを実行するときは、与えられた文字列を分解して解釈してくれるgo-shellwordsがある。
  • 一定時間後のタイムアウトにはnet/httpにあるClient。Go1.17からはContextパッケージが追加され、こちらが標準に。
  • ゴルーチンの外部からの停止にはチャネルのクローズ(クローズは一度きり)のほかに、contextパッケージでも制御できる。
  • Ctr+Cでコンソールが止まるような動きはOSの機構の「シグナル」。Goではこのシグナルを検知して無限ループを止めたりといった制御もできる。独自シグナルも定義可能。

 前章と被るところもありますが、この章も様々なテクニックが述べられています。いろんなライブラリが提供されているのですね。

Go忍者の技を学ぼう

第4章 コマンドラインツールを作る

 プログラミングでのアプリづくりの基本、CLIツールを深く扱った章。

  • バイナリを配布しさえばよい、OSをまたいだクロスコンパイルも可能、コンパイル言語ならではの良パフォーマンスが、Goでコマンドラインツールを作る際の利点。
  • メルカリ社内でも、Macで開発した社内ツールをWindows Serverの本番環境で使ったりしている。データ送受信やファイルアップロードにゴルーチンの並行処理が活躍。
  • CLIツールのインターフェースには2種類ある。
    • シングルコマンドパターン > 命令 [options] [<args>] UNIX哲学に基づいたシンプルな入力
    • サブコマンドパターン > 命令 [options] <コマンド> [<args>] ひとつのツールで多くのタスクをこなしたいときに。
  • リポジトリ構成
    • そのツールのバイナリがメインの時はルートにmain.goを置き、lib/配下にライブラリを置く。
    • ライブラリがメインの場合はライブラリをルートに置き、cmd/{機能の名前}/main.go のように置く。
  • 引数の取得はflagパッケージで機能が揃っている。ポインタを使わない変数に格納するやり方が吉。-h-helpでメッセージを出すやり方も標準装備。
  • -p-portのようなショートオプション、ロングオプションを別に定義するのが煩雑だったらパッケージもある。
  • os.GetEnv("{環境変数名}")環境変数を取得して引数のデフォルト値にするのもヨシ。
  • フラグの定義はパッケージ変数に書いてもよいが、本章の作者さんは関数内に書くルールにしている。パッケージに書くと可読性が下がりComposableでなくなるから。

 パッケージ変数に書くと変数のスコープが大きくなるので、追うときに大変になるのだろうなというのは素人目にもなんとなく納得です。

  • 標準のflag.IntVarのコードを実際に見てカスタマイズする方法。自分で*flag.FlagSet型を新しく定義し、独自のエラー処理を入れたりメッセージの出力先を標準出力に変えたりもできる。
  • flagパッケージはintやstring、boolなどに対応しているが、カスタマイズする方法。内部のflag.Value型インターフェースを使い、文字列配列用に拡張する例。
  • サブコマンドを持ったツールを作るには主要パッケージが5つほどある。そのうちmitchellh/cliの使い方。Commandというインターフェイスの中に関数を定義することで、サブコマンドを追加していく。

 最後は戻り値周り。

  • CLIツールはシェルなどから呼び出されるので終了ステータスコードが重要。0が正常で1以降がエラー。os.Exit()の引数に渡す値をiotaを使った定数で用意するとよい。
  • os.Exit()を呼ぶとdefer文が実行されずに終了するので注意。作者さんはmain関数の中で呼ぶ縛りにしている。
  • flagパッケージはエラー出力先が標準エラー出力なので、標準出力にも変えられる。
  • 内部関数の中で何かをしてエラーの際は変数errを単に返すのでなく、fmt.ErrOf("{エラー内容}", err)を返すようにするとエラー内容が分かりやすい。
  • ツールの実処理をRun(args []sring) のような内部関数で書いて戻り値を終了ステータスコードにしておく。するとテスト用関数でこのRun関数を呼ぶことで戻り値のテストができる。
  • さらにCLI構造体にio.Writer型でoutStreamerrStreamを持たせる。Run関数をこの構造体のメソッドとして紐づける。こうするとRun関数実行時に結果の出力をCLI構造体内に持たせられるので、テストができる。

 おおお、インターフェースはこういう風に上手く使えるのか...!と分かったりする、こちらも実践的な章でした。CLIツールの開発はGoプログラミングの基本の練習にもなりそうです。

Go忍者の技を学ぼう

第5章 The Dark Arts Of Reflection

 使わないに越したことはない...と但し書きしつつ、コンパイル時に判断できない箇所まで動的に操作を行える高等技、reflectパッケージを語る章。

  • Goではswitch x.(type) { } の型アサーションで変数の型を判断できる。ここでmap[string]string などは判断できるがmap型かは判断できない。これらを可能にするのがreflectパッケージ。
  • rv :=reflect.ValueOf(p) のようにして引数の型名、ポインタかどうか、実際の値が取得できる。
  • rv.Field(0)のようにして構造体の中を取得したり書き換えたりできる。ただし他のLL系言語のように型情報の動的変更や追加はできない。
  • rv1 := reflect.ValueOf(1)のようにしたあと、rv1.Kind()で対象の型を事前に判別して処理を分岐できる。なおJavaのReflectionのようにクラスの名前から取るのではなく、Goでは有効な値からでないと取れない。
  • 対象がmapだったらキーの型を判別したりできる。構造体ならField(n)でn番目を取得。
  • 構造体のフィールド名の後に X int "説明" のように書くタグ。これもreflectを使うと取得できる。パッケージで使われたりしている。
  • 値の動的生成もできる。Perlでクラスの名前からコンストラクタを呼び出したり、Rubyで動的にクラスを作ったりメソッドを付け加えたりするテクニックはGoではできない。
  • 構造体のフィールドが小文字始まりでパッケージ外からアクセスできない場合、reflectでも取れない。PkgPath要素を使うことで事前に確認可能。
  • ポインタの変数を操作する時は、ポインタが指している構造体を取得してから操作する。
  • 構造体の1番目のフィールドにrv.Field(0).SetInt(999)のようにセットする際、型が違うとそこでpanicになる。事前にCanSet()で確認できる。
  • ある型があるインターフェースを満たしているかは、reflect.TypeImplementsメソッドで確認できる。
  • 並行処理の select 文による分岐でチャネルが動的に増える場合も、reflectを活用すると分岐を増やさずに対応できる方法がある。
  • なおreflectを使った処理は、基本的に普通に書く際より実行速度はかなり下がる。各種ベンチマークあり。

 私的にはJavaのReflectionを使ってprivateなメソッドにアクセスする方法やらを知った時のようなあの背徳感、禁じられた秘密を知った時の興奮が蘇りました(うそ) これがGoの禁断のDark Arts...!
 本章にある通り、どうしても必要な時だけ力を解放してよい黒魔術なんですね。

Go忍者の技を学ぼう

第6章 Goのテストに関するツールセット

 6章は本格開発にはおなじみ、テスト周りの話。

  • GOのテストへの戦略は「明示」と「シンプル」。言語と別にテストツールを使うのでなく、Go自身に組み込むという低レベル、低レイヤー的なアプローチをとっている。
  • 標準のtestingパッケージとgo test コマンドが担う。
calc_test.go

// 引数は固定、関数名先頭がTestで認識される
func TestSum(t *tesing.T) {
  if someFunc(1,2) != 3 {
    t.Fatal("テスト失敗時のメッセージ")
  }
> go test {フルパスのパッケージ名}
> go test -v ./calc // modulesで認識されていれば相対パスOK -v が詳細
// そのパッケージの*_test.go が対象で、ファイル名先頭が _, . なら無視
> go test -v run {関数名} // 関数単位で実行
> go test -v run "some$" // 関数名前方一致
// Go1.10からテスト結果がキャッシュ、ファイルの内容と環境変数未変更なら
// 使われる。明示的クリアが以下。
> go clean -testcache 
hi_test.go
// Examplesという機能。標準出力をそのままテストできる
func ExampleHi() {
  fmt.Println("Hi!")
  // Output: Hi!
}

// Go 1.7からUnordered outputがサポート
func ExampleUnorderedHi() {
  // mapをイテレートして出力する処理。結果は順不同
  
  // 期待する出力が1,2,3の順でなくても通る
  // Unordered output:
  // 1
  // 2
  // 3
}

> go test -v // 関数名がTestでなくても認識される
  • Goの標準ライブラリでもこのExampleが利用されている。(本書に載っているWriterの例は、このエントリ執筆時点ではもうコードに// Output:は書いてありませんでした)

pkg.go.dev

// Benchmarkで始まる関数はベンチマーク用
func BenchmarkXX(b *testing.B) {
  // 中でベンチマーク実行用の関数を呼ぶ。引数にbを渡す。
  benchHelper(b, {回数とか}, {対象の関数とか})
  // 実行用の関数の中で b.ReportAllocs() を呼ぶと、ループ回数や所要時間、
  // アロケートされたバイト数や回数を出力してくれる。

  // なおGo 1.17からこの関数内に複数ベンチマークを書けるサブベンチマークが加わった。
}

> go test -bench . // コマンド実行時

Goの標準パッケージ自体がtestingパッケージでテストされているというのも面白い。Examplesという機能もコード内のコメントで直接期待する結果を書いちゃうというアプローチも面白い。ベンチマークの機能もGo自体に入っているところなど、やっぱり一通りの機能をコンパクトに揃えているんだなあと思います。

  • Test Driven Test。例えば構造体に入力文字列と出力文字列の組を宣言、この構造体のスライスでテストデータを作成。テスト用の関数の中でfor文で回して確認。
  • reflect.DeepEqual(x, y interface{}) bool を使うと、xとyが等価かどうかを配列やスライス、マップでも中を逐一見て行って比較してくれる。
  • 複数のゴルーチンから同じ変数にアクセスして競合が起こらないか確認できる Race Detector の機能。
  • Test Main の機能
func TestMain(m *testing.M) {
  setup() // DBのインサートとか前処理が書ける
  exitCode := m.Run() // 同じファイル内のTest~メソッドを全部実行
  shutdown() // DBのクリア処理とか後処理
  os.Exit(exitCode)
}
  • Build Constraints もテストに活用できる
// +buid integ

Func TestForIntegration(t *testing.T) {
  // ...
}

> go test // 実行されない
> go test -tags=integ // 実行される。結合テストのみとか
  • パッケージ変数でアクセスするURLリストを定義。関数でそのパッケージ変数を使う処理を実装。Testする関数の中ではこのパッケージ変数を書き換えて、不要なHTTPアクセスを減らしたり...というテクもある。
  • 構造体とインターフェースを上手く使うと、テスト時はダミーデータを返す同名関数を定義して同じインターフェース型でそちらを使うように置き換えたりもできる。
  • net/test/httptest を使うと、テスト用のサーバーを立ち上げることできる。8080ポートが使用中の時などに。
  • go test -cover でテストカバレッジが取得できる。

 インターフェースによる置き換え例のところが今いち理解しきれなかった...無念!
Go言語付属の基本機能だけでもテスト周りはいろいろな事ができようになっているのが分かりました。

Go忍者の技を学ぼう

第7章 データベースの扱い方

最後はDBアクセス周りと、最後にWebアプリも載っています。作者さんが実際にドライバを開発しているというのが強い!

github.com

  • database/sqlパッケージがアプリとDBのドライバの中間に位置して中を取り持っている。ドライバは2種類。
    • GoからC言語を通して操作する"cgo"。さらにSDKインストールも必要。
    • Go言語だけで実装されている"pure go"。goのパッケージをgo install しておくだけ。
  • DB接続の基本は簡単で db, err := sql.Open("postgres", {DSN文字列})
  • そのあとdb.Ping()しないと実際に接続しないドライバもあるので注意。
  • result, err := db.Exec("{UPDATEのSQL文}")SQL文実行。名前付きプレースホルダも使えるし使うべき。
  • 戻り値の変数からは result.RowAffected()で更新レコード数、result.LastInsertId()で最終挿入IDを取得したり。
  • rows, err := db.Query("{SELECT val1, val2...}") で検索。
  • rows.Next()でループし、以下のように変数のアドレス指定で、レコードのカラムの内容がとってこれる。
var int val1
var string val2
err = rows.Scan(&val1, &val2) // テーブルのval1, val2というカラムの中身が取れる
  • row := db.QueryRow("SELECT val1 from users WHERE id=$1", 1)のようにすると単一行の検索。結果0行だとScanの呼び出しでエラー。
  • DBの型とGoでの型はある程度database/sqlが変換してくれる。Scanに与える変数をinterface{}型で型宣言してもよいが、取得できた後で型アサーションで変換がいる。
  • 上の方法でコードが長く汚くなってしまう場合は、値変換の仕組みを実装するテクがある。
type MyType string //エイリアスを作る

// インターフェースを定義
func (mType *myType) Scan(src interface{}) error {
  switch x := src.(type) {
    //型ごとに分岐して 実体の*mType に MyType({変換した文字列の値}) を代入。
    // 最後はnilを返す。途中エラーが起こったら戻り値はそのエラーが入る。
  }
}

// DBアクセスの結果がrowsに入っているとする
var age MyType
err = rows.Scan(&id, &name, &age)
// 3番目のフィールド内容が変数ageの指すアドレス先に入る過程で、上の処理が走る
  • 上のようにScanには常にポインタを渡せば、カラムがnull許容の場合、結果がnullだったら変数もnilになる。
  • SELECT * した場合はrows.Coumns() するとカラム数が取得できる。すべてinterface{}型の配列を作って全カラム取得したり。
  • しかしこれが面倒なのでGoではNullStringNullInt64など専用の構造体を用意している。
var name sql.NullString // この方法ではポインタの*不要
rows.Scan(&name)
fmt.Println(name.String) // 結果がNULLだったら""にしてくれる
  • ただしNullString系はJSONに対応していない。以下のように自前で構造体を作るテクがある。
type MyNullString struct {
  s sql.NullString
}

func (mns *MyNullString) Scan(value interface{}) error {
  // mns.s.Scan(value) を呼ぶだけ
}

func (mns MyNullString) String() string {
  // 文字列化するメソッド
}

func (mns MyNullString) MarshalJSON() ([]byte, error) {
  // 引数チェックの後にjson.Marshal(mns.s.String)を呼ぶ
}

func (mns *MyNullString) UnmarshalJSON(data []byte) error {
  // json.Unmarshal を呼んだ結果をmns.s.String と mns.s.Validに入れてnilを返す
}


// 実際に使う側
type SampleUser struct {
  ID int64
  Name MyNullString
}

var user SampleUser
err = row.Scan(&user.ID, &user.Name)
// この変数をJSON化処理や標準出力に渡してもうまく動作する
  • GoにもこうしたScanメソッド周りを透過的に扱ってくれるORMのパッケージがいくつかある。Beego, GORM, XORM, go-pg, go-gorp, upper, sqlboiler の7つが有名。本書では詳しくベンチマークを計測している。(!)
  • このうちgorpが本書のおすすめ。gorpがMultiInsertだけ遅いのは、他のORMパッケージはRDBMS固有のバルクインサートを使っているため。
  • 構造体でテーブル定義相当のものを作る。この時タグでカラムの属性を指定。
  • gorpでは専用メソッドでテーブル作成もできてしまう。
  • 定義した構造体の実体を専用メソッドに渡すようにして、1レコードを挿入したり。PreInsertPreUpdateのフック関数で処理を挟むこともできる。

DB周りもだいたい一通りの方法はGoでも用意されているのだなというところ。ORMのベンチマークを測定していたり本書はしっかりしています。
 最後はRESTサーバを作ると題してWebアプリも触れられています。

  • 企業開発でバグ対応も安定、ドキュメントもあり、機能が揃っていることから本書ではWebフレームワークの代表格、echoを解説。

github.com

  • 以下のようにGETやPOSTというメソッドの第2引数に処理を渡す形で、簡単にWebサーバーが動いてしまう。
func main() {
  e := echo.New()
  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello")
  })
  e.Logger.Fatal(e.Start(":8080"))
}
  • e.Static("/", "static/") のように書くとstatic/index.html を表示してくれる。
  • APIを作る場合はe.GETe.POSTで同様に。DB処理も割と短く書ける。
  • validatorというパッケージを使うと、DB登録前にバリデーション処理が書ける。構造体でValidatorValidateメソッドを定義、e := echo.New() した後の変数eに渡す。
  • もっと凝った画面例として本書ではVue.jsで生成。サーバー側と通信してコメント一覧をとってきたり登録したり。本章の作者さんのリポジトリにサンプルあり。

github.com

 明記されてないですがDBアクセスのところで変数dbmapが出てきたので、前節のgorpを使っているようですね。Webアプリもだいぶ簡単に書けてしまうんですね。
 このecho-example リポジトリも拝見したのですが、Goのコードは1ファイル200行ぐらいでもうCRUDが一通り揃っていてこれだけで済んじゃうのか...! という感じ。
逆に開発規模が大きくなっていってファイルを分割する際は、どうやるのがいいのかなと思いました。

 そして本章の最後は、これから時代が進んでこれらのライブラリが古くなってもプログラマの皆さんがやるべきことは変わりません、「新しい技術要素を調査し、あらゆる候補を比較するためにベンチマークを取ることです」と締めていて熱い。この姿勢を見習わなければなあと思いました。

Go忍者の技を学ぼう

まとめ:Goニンジャの現場の技を学べる本

 このエントリを書いているのが2022年なので古くなっていないかなと若干心配だったのですが、Go Moduleの話など以外はそれほど困るところはなく読み進められました。対象としてはもうGoの言語仕様は一通り分かっている方や仕事で使っている方、入門するなら2冊目以降の本になるでしょう。
 完全には理解できないところもあったのですが、読みながら「あーGo言語だとこういう時こうするのか~!」となる箇所が多かったです。他言語の経験者なら皆さん同じように思うのではないでしょうか。ネットでも現場のGopherたちから好評の1冊ですね。Go使いの知見が詰まった一冊でした。

作者のおひとりのmattnさんの発売時の記事。 mattn.kaoriya.net

作者のおひとりのsuzukenさんの記事。

suzuken.hatenablog.jp

似た立ち位置の本としては、2021年に出た『エキスパートたちのGo言語 一流のコードから応用力を学ぶ』があります。

最新はオライリー本の『実用 Go言語 ―システム開発の現場で知っておきたいアドバイス』が2022年4月に出たばかりですね。

Go言語にかかわる本は以下のエントリでもまとめています。

iwasiman.hatenablog.com