Golangの現場の忍者(うそ)から学んでみよう
ちょうど入門していたので入門書の次に1冊読んでみた本です。著者陣は面白法人カヤック、メルカリ、VOYAGE GROUPなどなど各社より、Goを実践している重鎮の方々。2016年に初版が出た後、表紙も忍者ゴーファーくんたちに変わり内容も全面改訂された改訂2版が2019年に出ました。以下、読書記録と感想です。
- Golangの現場の忍者(うそ)から学んでみよう
- 第1章 Goによるチーム開発のはじめ方とコードを書く上での心得
- 第2章 マルチプラットフォームで動作する社内ツールのつくり方
- 第3章 実用的なアプリケーションを作るために
- 第4章 コマンドラインツールを作る
- 第5章 The Dark Arts Of Reflection
- 第6章 Goのテストに関するツールセット
- 第7章 データベースの扱い方
- まとめ:Goニンジャの現場の技を学べる本
前書きでは本書の作者さんたちからは、Goのメリットとしてはパフォーマンス、メモリ管理からの解放、コンパイル速度の速さ、コード整形、言語仕様のシンプルさ、シングルバイナリの手軽さが上げられています。
「人類にメモリ管理は難しすぎる」と書いてあったりして、やはり苦労する局面はあるのだろうなあと思います。ブラウザのChromeやFirefoxの歴代の重大バグのほとんどはもうメモリ回りらしいですね。
第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の環境構築で一緒に入るものもありますが、基本的なツールでもいろいろあるのだなあという感じ。
- ディレクトリ構成の代表的な例。
- ファイル分割は、
type Hoge
の定義+それに対するメソッドで1ファイルにするとよい。 - クラス定義の階層と名前空間が対応している他の言語の感覚でパッケージ分割するのはNGで、パッケージ数はそれほど多くならないはず。
- 「他のプロジェクトから使えるかどうか」がパッケージ判断の基準。
- サブパッケージのimportは絶対パスを推奨。
Go Modules
の使い方
ディレクトリの構成の話はとてもありがたいです。他の言語だと/src
などいったん掘ってそこにコードを置くことが多い(気がする)ので、Go言語はこのへん若干独特ですね。
そしてお役立ちなのが「1.4 Goらしいコードを書く」の節。
- 慣れてきたらEffective Goを読むのがおすすめ。
- 適当に
panic
を使うのはNGで、関数の末尾の戻り値でerror
を返す。呼び出し元はエラーチェックをする。Goでは愚直でも素朴に描く。 - 正規表現の
regexp
パッケージは最悪ケースを想定しているのでパフォーマンスが悪い。文字列操作がstrings
パッケージで間に合うならそちらを。 - どうしても正規表現を使う場合は動的生成でなく初期化時に行う。
map[string]string
などを安易に多用せず、構造体を使ったほうがよい。reflect
パッケージを使ったリフレクションは基本避ける。黒魔術に手を出さない。- Goは継承がない。巨大な
struct
は作らず、小さなものを組み合わせる - 並行処理も使いすぎると可読性が落ちたり見つけずらいバグが出てくる。基本は直列処理でコードを書き、時間がかかりそうな箇所で並列処理を活用。
- 標準パッケージのコードを読むと学びになる。
- CIで
go vet
やgolint
の検査も一緒に行うと良い。 go build
の際に引数でバイナリに情報を埋め込んだり、タグを指定してdebug/prodを切り替えたりするテクもある。- 常駐プロセスはモニタリングするとよい。
正規表現のパフォーマンスが悪いなんて話はへぇーと思いました。かなり堅牢にしっかり作られているように見えるGo言語でもこういう特徴あるんですねえ。まさに現場からのお役立ち情報です。
第2章 マルチプラットフォームで動作する社内ツールのつくり方
社内には様々なOSや環境があり、ランタイムの別途インストールが難しい場合もある。こんな時こそバイナリを配布しさえすればよいGo言語の出番だ...ということで、社内ツールを題材に様々なテクニックを紹介する章。
- Unix系OS/Windowsでのパス操作の違い問題は頻出。Goでは
path/filepath
パッケージで物理パス回りを吸収できる。httpなどの論理パス操作にはpath
パッケージなので間違わないこと。 defer
を活用。呼び出した順番(≒上から下に書いた順番)と逆順に実行される。関数を呼んでオブジェクト生成の次の行で変数err
のチェック、その直後でdefer
で後処理(ディレクトリ削除など)をするように習慣づけると、たいていGoでは問題ないコード構成になる。- Windowsでコマンド引数からとった値は文字コードCP932と想定、いったんUTF-8にデコードしてからGoプログラム内で扱うなど、UTF-8を積極的に使う。
あーこのへん確かに問題が出そうなところだな~と思いながら読みました。Javaでtry-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.go
やxx_linux.go
などのファイル名で実は判別してくれる。それぞれのファイルの中に固有の初期処理を描くなど。 - もうひとつはファイル内に
//+build
でコメントを書く方法もある。 - Goのコード内からC言語を呼び出すには
cgo
というコマンドを使う。 - 拡張子.pcのファイルが付属しているライブラリ類では、
pkg-config
コマンドでオプションを知ることができる。 - Go単体では実はプロセスをデーモン化できない。他のアプリの力を借りるほうが早い。Linuxで作者さんのおすすめはupstart。Windowsではnssmというアプリがおすすめ。
- なんでもGo単体で解決しようとする必要はない。アプリはそれがやろうとしていることだけをうまく実装した方が良い結果になる。
このへんはUNIX思想をしっかり受け継いでいるなあと思いました。
- Goの実行ファイルはシングルバイナリだが、テンプレートや画像ファイルなどを入れたい場合はアセットツールを使う。
statik
:/public
にindex.html
などを入れてバイナリに埋め込める。コード内からでもこのファイルにアクセスできる。- Goでは
//go:generate
というマジックコメントでコマンド実行ができるので、そこにstatikと書くとビルド時に静的ファイルからstatik.go
が生成、一緒にビルドされて実行。 packr
: 同じようにコード内からファイルにアクセス可能。他にファイル一覧を取得したりもできる。
- なお本書には載っていませんが、Go1.16から標準のembedパッケージでstatikと同じことができるそうです。
Windowsアプリを作る際はビルド時の
-H
オプションでコンソールが出ないようにしたり、アイコンのリソースファイルも含められる。- 置き場所はLinuxは
$HOME/.config/
配下、Windowsは%APPDATA%\{アプリ名}\config.json
- ホームディレクトリ取得には
os/user
パッケージは使わない方がよい。環境変数から現在パスを取得して設定ファイルを読むのがおすすめ。 json.Marshal
よりjson.MarshalIndent
を使うとJSON文字列がきれいになる。
あーこうやって作るのか~と分かる実用的な章でした。設定ファイルはやはりJSONが主流なんですね。Webアプリはアセットツールを使ってexeの中に入れちゃうんですね。
第3章 実用的なアプリケーションを作るために
こちらも実際の使用、運用に耐える本格アプリでのTipsを紹介する実用的な章。作者さんがご自分で作ってGitHubで公開しているというプル型デプロイツールStretcher、FluentDにログを送信するfluent-agent-hydraがさらっと出てきて、つよエンジニア!という感じです。これらのOSSを題材に紹介していきます。
実用的なアプリの要素:持っている機能を容易に調べられる、良パフォーマンス、多様な入出力(HTTP/ファイル/標準入力など)、人間に読みやすいログ出力、想定外の場合に安全に停止できる
コードにビルド番号を埋め込み、実行時の引数で
-v
などが来たらflag
パッケージで取得して表示すればよい。go build
時に-Idflag
を指定するとビルド時に引数で任意の変数設定もできる。最新バージョンを識別する方法もいくつかある。
Goは
io.Reader
やWriter
インターフェースで入出力を順次処理。自動でバッファリングはされないので、bufio
を使うとバッファサイズを指定したりして活用できる。- ライブラリの
go-isatty
を使うと出力先が端末か判定できるので、端末ならバッファしないなどの分岐もできる。
他の動的言語での繰り返し処理と比較して、Linuxでstraceコマンドを使ったシステムコール発行回数比較の実例まで載っており実践的です。おおー低レイヤーの世界に近づいている...という気持ちになります。
- 複数ソースからコンテンツを取得する際の実装例。
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ではこのシグナルを検知して無限ループを止めたりといった制御もできる。独自シグナルも定義可能。
前章と被るところもありますが、この章も様々なテクニックが述べられています。いろんなライブラリが提供されているのですね。
第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
型でoutStream
とerrStream
を持たせる。Run
関数をこの構造体のメソッドとして紐づける。こうするとRun
関数実行時に結果の出力をCLI構造体内に持たせられるので、テストができる。
おおお、インターフェースはこういう風に上手く使えるのか...!と分かったりする、こちらも実践的な章でした。CLIツールの開発は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.Type
のImplements
メソッドで確認できる。 - 並行処理の
select
文による分岐でチャネルが動的に増える場合も、reflect
を活用すると分岐を増やさずに対応できる方法がある。 - なおreflectを使った処理は、基本的に普通に書く際より実行速度はかなり下がる。各種ベンチマークあり。
私的にはJavaのReflectionを使ってprivateなメソッドにアクセスする方法やらを知った時のようなあの背徳感、禁じられた秘密を知った時の興奮が蘇りました(うそ) これがGoの禁断のDark Arts...!
本章にある通り、どうしても必要な時だけ力を解放してよい黒魔術なんですね。
第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:
は書いてありませんでした)
// 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言語付属の基本機能だけでもテスト周りはいろいろな事ができようになっているのが分かりました。
第7章 データベースの扱い方
最後はDBアクセス周りと、最後にWebアプリも載っています。作者さんが実際にドライバを開発しているというのが強い!
database/sql
パッケージがアプリとDBのドライバの中間に位置して中を取り持っている。ドライバは2種類。- 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では
NullString
、NullInt64
など専用の構造体を用意している。
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レコードを挿入したり。
PreInsert
やPreUpdate
のフック関数で処理を挟むこともできる。
DB周りもだいたい一通りの方法はGoでも用意されているのだなというところ。ORMのベンチマークを測定していたり本書はしっかりしています。
最後はRESTサーバを作ると題してWebアプリも触れられています。
- 企業開発でバグ対応も安定、ドキュメントもあり、機能が揃っていることから本書ではWebフレームワークの代表格、
echo
を解説。
- 以下のように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.GET
やe.POST
で同様に。DB処理も割と短く書ける。 validator
というパッケージを使うと、DB登録前にバリデーション処理が書ける。構造体でValidator
とValidate
メソッドを定義、e := echo.New()
した後の変数e
に渡す。- もっと凝った画面例として本書では
Vue.js
で生成。サーバー側と通信してコメント一覧をとってきたり登録したり。本章の作者さんのリポジトリにサンプルあり。
明記されてないですがDBアクセスのところで変数dbmapが出てきたので、前節のgorp
を使っているようですね。Webアプリもだいぶ簡単に書けてしまうんですね。
このecho-example リポジトリも拝見したのですが、Goのコードは1ファイル200行ぐらいでもうCRUDが一通り揃っていてこれだけで済んじゃうのか...! という感じ。
逆に開発規模が大きくなっていってファイルを分割する際は、どうやるのがいいのかなと思いました。
そして本章の最後は、これから時代が進んでこれらのライブラリが古くなってもプログラマの皆さんがやるべきことは変わりません、「新しい技術要素を調査し、あらゆる候補を比較するためにベンチマークを取ることです」と締めていて熱い。この姿勢を見習わなければなあと思いました。
まとめ:Goニンジャの現場の技を学べる本
このエントリを書いているのが2022年なので古くなっていないかなと若干心配だったのですが、Go Moduleの話など以外はそれほど困るところはなく読み進められました。対象としてはもうGoの言語仕様は一通り分かっている方や仕事で使っている方、入門するなら2冊目以降の本になるでしょう。
完全には理解できないところもあったのですが、読みながら「あーGo言語だとこういう時こうするのか~!」となる箇所が多かったです。他言語の経験者なら皆さん同じように思うのではないでしょうか。ネットでも現場のGopherたちから好評の1冊ですね。Go使いの知見が詰まった一冊でした。
作者のおひとりのmattnさんの発売時の記事。 mattn.kaoriya.net
作者のおひとりのsuzukenさんの記事。
似た立ち位置の本としては、2021年に出た『エキスパートたちのGo言語 一流のコードから応用力を学ぶ』があります。
最新はオライリー本の『実用 Go言語 ―システム開発の現場で知っておきたいアドバイス』が2022年4月に出たばかりですね。
Go言語にかかわる本は以下のエントリでもまとめています。