他の言語になれた人が、初めてGoを書いた時にわかりにくいな、と思った部分はどのようなところがあるのか、難しいポイントはどこか、という情報を自分の経験や、会社の内外の人に聞いたりしてまとめてみました。まだまだたくさんあるのですが、多すぎるのでまずはこんなところで。コンテナで開発することがこれからますます増えていくと思われますし、その時にコンテナとの相性が抜群なGoをこれから使い始める人もどんどん増えていくと思います。
Goは特に言語のコアをシンプルに、何かを実現するときはそのシンプルな機能を組み合わせて実現しよう、というコンセプトです。つまり、他の言語で実現したいこと・できていることに比べて、Goは組み合わせ(イディオム)でカバーする領域が広くなります。そのあたりのとっかかりになる情報を提供することが、これからGoを触る人にとってつまずきを減らすことになると思います。
Go Conference’19 Summer in Fukuokaではこちらの中からいくつかピックアップをしましたが、こちらが今の所の完全版です。
見え方の違い
Goになれている人となれていない人では、同じコードを見た時にも見えている景色がだいぶ違いますし、コードを書くときの頭の使い方もだいぶ違います。
再生 vs 再認
認知心理学では、2つの記憶のモード、再生と再認を考えます。
再生は、過去の記憶を何もインプットがなくても思い出せる状態です。再認は、「これは体験したことがありますか?」と問われたら「ああ、これは体験したことがある」と思い出せる状態です。
再生をするには完全に記憶しきっている状態にならないとダメです。慣れている人は、数多くのパターンを記憶しており、それを再生することでコードが書けますし、他の人のコードを見た時にも、過去のパターンと照らし合わせて「より良いコード」パターンを思いついたりします。
再認の方が難易度は低いです。ドキュメント、サンプルコード、ネットで調べた情報などを辿りながら、それを組み合わせて実装します。時間もかかりますし、検索で出てこないパターンだとパフォーマンスが落ちます。ですが、なんどもなんどもコードを見て繰り返し再認していくと、再生でコードが書けるようになります。このドキュメントは再認の効率アップがゴールです。
とはいえ、必ずしも全員が再生レベルになる必要はありません。たとえGoに慣れていても、普段使わないパッケージ(cryptoパッケージの暗号化とか)を使う場合は再認で(サンプルのコピー&ペーストで)コードを書くことになるでしょう。上級者でも、自分用のスニペット集を作ることで、記憶の能力の節約しつつパフォーマンスは落とさないということをしますね。
アンラーニング
すでに学んでいる知識がかえって学習の妨げになりがちです。このバイアスを除外して(客観化して)、あらためて学ぶというのはなかなか難易度の高いことです。自分の知識をリセットして(組み替えて)楽しむSF好きとか、逆転裁判好きとか何かしらのプラスアルファの能力が必要じゃないでしょうか?
受けての言葉で差分を表現して教えて上げるというのはこのスタートのつまずきを解消するてっとり早い方法です。そして一通り自分で手が動くようになればそこでどんどん新しい考えが定着していきます。それまでは温かい目で見守る必要があるでしょう。
なお、うまくアンラーニングできるということは、自分がそれまで持っていた知識が客観化されることになるので、過去に学んだ言語が勝手に上達することもありえます。
実装パターン集
では説明していきましょう。そのうちカテゴリーに分けるかもしれません。以下のような形式で統一します。
- これを使うときのGoの作法はなにか?(慣れている人がよく選ぶものはなにか)
- この言語でやっていたように〇〇したいがどうすればいいのか?
- できない場合はその理由
- 複数ある場合はそれぞれの選択肢とトレードオフ
- Goにこの機能があるけどどう使うのか?
パッケージ
Q: リポジトリはそもそもどこにおけば良いですか?
適当なフォルダを作業フォルダにするのは可能ですか?
A: Goは$GOPATH
という環境変数のところにいろいろ置きます。デフォルトでは$HOME/go
です。これはソースコードだけではなく、ビルド済みのライブラリなどです。とりあえず作業フォルダを作ったら、そこが$GOPATH
となるように、direnvなりで設定されるようにしましょう。
プロジェクトごとに完全に分けたいのであれば、トップの$GOPATH
用のフォルダを必要なだけ切って利用します。そこまで厳密でなければデフォルトの$HOME/go
で良いと思います。
$GOPATH
以下は次のようになっています。appが自分の作りたいアプリケーションだとしたら、そこにフォルダを作り、git init
してしまえば良いです。go get
を行ってもここにダウンロードされます。
$GOPATH |
複数のリポジトリに分離したプロジェクトを作るときも同じ場所に置きます。ただし、依存ライブラリ側は、GitHub等からダウンロードされて、pkg/mod以下に入れられたバージョンを利用しようとします。一緒に開発したい場合、わざわざ不安定なバージョンをGit pushしなければならない、というのは不便でしょう。幸い、go.modにはreplaceという機能が使えます。appがlibraryを使う場合は、appのgo.modに次の行を加えておくと、同じ場所にあるフォルダを参照してくれます。複数リポジトリを一緒に変更しつつ動作検証するにはこの方法がベストです。
replace ( |
memo: Go 1.12では$GOPATH
以下はgo.modが自動では有効にならないため、環境変数設定で、GO111MODULE=on
を入れておきましょう。あと数ヶ月で不要になりますが。
環境構築にはあまり凝りすぎないで、デフォルト多めの方が、世間の情報とのずれが少なく、後から見返すときに楽です。凝りすぎたMakefileやらシェルスクリプトやらは引き継ぎのコストが多くなったりするし、自分でも忘れてしまったりしがちです。
Q: 関数や構造体は、パッケージ(ディレクトリ)やファイルにどのように配置していけばいいか?
各言語で、パッケージ、モジュール、ファイルの扱いがかなり違います。Goでは関数をどのように格納していけばいいのですか? 例えば、Javaではフォルダがパッケージ。ファイルがクラスと対応づけられています。JavaScriptやPythonはファイル単位でモジュール、ディレクトリはデフォルトのファイル(index.js
や__init__.py
)を自動で読み込む動作をします。
A: Goではディレクトリがパッケージになります。ディレクトリに含まれるすべてのファイルは同一のパッケージ名でなければなりません。一方、ファイル間で要素を移動してもビルド時には差はありません。
関数や構造体、変数などはディレクトリ内部のファイルに書きます。1つのファイルには1つの何かを書くというルールはありません。このあたりはPythonやJavaScriptに近いと言えます。
a.goとb.goが同じディレクトリ内にあった場合、a.goで定義された要素はb.goから何も宣言せずに利用できます。スコープに関してはどちらかというとJavaに近い感じです。そういった点で、ファイルに関してはどこに何をおいても、ファイル間で移動してもビルド時には差はありません。最初は1ファイルでなんでも入れておいて、大きくなってきたときに、パッケージの中で、コードなどをグルーピング化する単位でファイルに分ければ良いでしょう。
唯一、条件コンパイルで、Windowsの場合だけ、Linuxの場合だけ利用される関数といった条件コンパイルを実現するときは、ファイル単位でビルドに含めたり除外したりしますので、その単位で分けます。
Q: プロジェクトを作るときのフォルダ構成の定番はどのような構成ですか?
新規で作るときにどのようなプロジェクト構成にすればよいでしょうか?
A: ライブラリなのか、ライブラリ+CLIツールも提供するのか、ウェブサーバーなのかで変わります。
これはGoのリファレンスなどには書かれていませんが、一般的な慣習として使われているフォルダ構成です。
ライブラリの場合は、トップフォルダがライブラリのルートになります。リポジトリのルートで、go mod initをします。code.goの先頭は pakckage yourlib
になり、go.modの方はmodule github.com/yourname/yourlib
になります。READMEには go get -u github.com/yourname/yourlib
でインストールしてねって書きます。
+ github.com |
ライブラリが、おまけでCLIツールを提供する場合は、cmdフォルダを作ってその中にコマンド名のフォルダを作り、その中に実行ファイルのソースをおきます。Goの場合ビルドを実行したフォルダ名がデフォルトで実行ファイルの名前になります。go modulesで成果物の名前を設定できるようになりましたが、それ以前からの慣習として一般的に使われています。
複数のコマンドを提供するときはcmd以下に複数フォルダを作ります。各実行ファイルは当然package main
になります。READMEには go get -u github.com/yourname/yourlib/...
でインストールしてねって書きます。
+ github.com |
ウェブサービスのプロジェクトの場合は、トップフォルダがアプリケーションのルートになります。リポジトリのルートで、go mod initをします。実行ファイルなのでmain.goの先頭は pakckage main
になり、go.modの方はmodule github.com/yourname/yourapp
になります。あるいは、go mod init yourapp
と実行しても良いです。その場合は、module yourapp
となります。READMEには go get -u github.com/yourname/yourapp
でインストールしてねって書きます。
+ github.com |
Q: パッケージやフォルダの名前には何を使えばいいか?
パッケージやフォルダには自分で名前を設定できますが、何を使うのが良いでしょうか?
A: 基本的にはEffective Goで説明されている作法(簡潔な何をするパッケージ化が明確になる一つの単語、小文字、フォルダ名と同じ)に従えば良いでしょう。
なお、この推奨はフォルダ名と同じというのが入っているので、フォルダ名も必然的に同じルールが適用されます。複数の単語で構成させたい場合は、 encoding/json
のようにフォルダを分け、末端のパッケージは pakcage json
とするのが一般的です。
パッケージはインポートしたあとに利用するシンボル名としてデフォルトで利用されます。簡潔で短い方が、プログラムが短くなります。GoはJavaのようななるべく明示的な名前を使う作法とは逆に、記憶の再認が可能な限り短くするのが作法です。また、フォルダ名と違うと、import
文とプログラム中で利用されるシンボルがずれるため、コードを読むときに困ります。
ただし、この1単語に沿わないケースが2つほどあります。
テスト用パッケージ
たとえば、 slices
という名前のフォルダを作り、 pakcage slices
のような名前のパッケージでコードを書くとします。通常はGoの場合は同一フォルダ内のファイルはすべて同じパッケージにしなければなりませんが、 _test
が末尾に入っている名前(ここではpackage slices_test
)だけは特別に設定可能です。
リリースされるライブラリには含まれない、テスト用の共通処理を書いたり、ユニットテスト自体をこの_test
パッケージで書くことができます。ただこの場合は本体のslices
とは別のパッケージ扱いになるのでimport文を書かないとslices
の提供する関数や構造体にはアクセスできませんし、公開されているものしかアクセスできません。exampleテストなどのブラックボックステストには逆にメリットですが、プライベートな要素のテストはできません。
xxxx-go, go-xxxx, xxxx.goフォルダ
たまに見かけるケースです。何か既製のライブラリのGoラッパーとか、Go移植とかで見られるものです。たとえばlxc開発元自体が提供しているgo-lxcなんかは、GitHubの同一階層に本体のlxcがあるので、それと区別するためにgo-が入っています。
なお、ハイフンとかピリオドはパッケージ名には使えません。この場合は package xxxx
のように、-goとかgo-とか.go部分を外した名前をパッケージ名につけることが多いです。
ビルド環境・ツール
Q: Goについて調べるといろいろなツールが出てきますが、何が本当に必要なんでしょうか?
それぞれのツールの役割、入れた場合のメリットとか、入れなかった時のデメリットについて教えてください。
A: とりあえず、go fmtだけはエディタの保存時に実行されるように設定しておけば良いです。
- go fmt: コードのスタイルを標準スタイルの設定する純正ツール。他の言語でいうところのPythonのblackとかJS/TSのprettier。Gitのマージでのトラブルが減ります。やらないとコンフリクト等が増えるかもしれません。
- go vet: コンパイルはできるんだけど、静的解析で問題が起きそうな箇所を見つけてくれる純正ツール。Go 1.10からはユニットテスト実行時に一緒に実行されるようになったので特に何もしなくてもご利益が得られる(当然テストはしてますよね?)
- その他のツール群: 基本的に静的チェックでエラーを見つけてくれるものが多いです。これらのツールをまとめてチェックしてくれるツールもあります。
Goはそもそもコンパイル言語であるので、変なコードはビルドすればわかります。型の整合性もいろいろわかります。何も入れなくてもスタート地点がかなり高いしgo vet自動実行もあるので、他の動的言語で徒手空拳で戦うのに比べたら遥かに安心感を持ってコードを書くことができます。
それ以上にいろいろチェックしたければ入れると良いと思います。
ドキュメント
Q: Goはドキュメントが充実していると聞いたのですが、リファレンスを見ても使いたい機能にどうアプローチすれば良いのかわかりません。
便利そうな関数あっても、それの利用方法がドキュメントを読んでも想像できません。ついつい、Qiitaなどのサンプルコードを探してしまいます。あと、どのようにソートされているのかわからず、探しにくいです。
A: ファクトリー関数とインタフェースの2つがわかると、読みやすくなります。
まず、Goは構造体そのものを使うときには、何も初期化しないで使うか、ファクトリー関数を使って初期化します。例えば、syncのWaitGroupとかRWMutexはゼロ初期化でも正常に動きます。一方、初期化が必要なものはファクトリー関数を使って初期化します。ゼロ初期化でも動くけどユーザーの利便性のためにファクトリー関数を用意しているものもあります。
ファクトリー関数(その構造体のポインタを返す)はgo docはその構造体のメソッドリストの中に入れてくれます。なので、「この構造体が使いたい」というときはまずファクトリー関数を見つけると、そこが解決の糸口になります。
// ゼロ初期化 |
ただ、その「ファクトリー関数を整列する」go docの機能が仇になって、読みにくくなるケースも多少あります。net/httpのGet/Postなどは単独で使ってHTTPアクセスをする関数ですが、Responseのファクトリー関数としてリストされてしまっています。少し、使い手側の感覚とはちょっと違いますよね? 関数と構造体の順番にソートされていて、単独で使える機能は関数のところだけを探しがちですが、構造体のファクトリーメソッドのところにも使える機能が隠されている可能性があります。
次はインタフェースです。例えば、先程の zip.Writer
を使いたい場合、 io.Writer
というものが必要というのはわかります。で、 io.Writer
を見ても、どう作っていいのかわからず、ここでお手上げになってしまう、というのがGoではありがちです。知識がついてくると、「os.Createで作ればOK、テスト用にはbytes.Bufferを使おうとかわかってくるのですが、どうしてもここは引っかかりがちです。
go docをローカルで動かして静的解析するとインタフェースを実装している構造体一覧とかもわかったりはします。Goが標準で提供している外部入出力系はこれでだいたいカバーできますが、io.Reader/io.Writerとかこのあたりはある程度暗記は必要かな、と思います。
しかし、例えば、 sort
パッケージのsort.Interface
なんかは、利用者がそのインタフェースを実装するのを期待しています(ので、そのインタフェースを作成してくれる関数はどこにもありません)。また、いくつか、ライブラリが提供する構造体のみが来ることを想定して引数がインタフェースになっていることがあります。作るにしても、ライブラリが提供する構造体を利用するにしても、ライブラリの提供者がきちんと動作可能なサンプルコードをドキュメントとして提供すべきですね。場合によってはソースコードのテストコードを読むとかが必要になるかもしれません。
ドキュメントが充実している文化とはいっても、書く人によって差がでたりわかりやすかったりわかりにくかったりするのはどうしても仕方がない面があります。
型・変数
Q: ウェブアプリケーションを開発しているが、型の恩恵がわかりません
JSONのマッピングのための型など構造体を実装する手間ばかりが多く、面倒です。
A: たしかに、型のメリットを一番体感しやすいのは他人の書いてくれたものを利用するときです。自分で書いたものを自分で利用する場合はメリットを感じにくいこともありますが、後から助かる保険になります。
特にJavaScriptからGoに来ると、今時のVisual Studio Codeが賢すぎて、型情報とか定義しなくても推論してしまうので、Goが面倒に感じる場面もあります。しかし、確実に後から助けてくれます。
- 他の人が書いたコードを読み解く時に、型情報がヒントになります。Visual Studio CodeやGoLandなどのエディタを使って入れば、「定義元にジャンプ」という機能が使えます。Goはすべての型をきちんと判定するため、確実に定義元にジャンプできます(ただしインタフェースから実装の構造体には飛べません)
- 1ヶ月以上たって自分のコードを読み解く時に、型情報がヒントになります。
- コード整理のためにあっちこっちに移動するときに、不一致があるとエディタがその場でエラーを教えてくれます。既存のコードに手を加えるのが楽になります。
あとは、歳をとって記憶力がなくなってくるとか、風邪をひいた、二日酔いがつらい、という状況では短期記憶能力が下がります。まずはビールを何杯か飲んでからコードを書いてみると良いかもしれません(ダメ絶対)。
おまけですが、JSONから構造体へのマッピングを作る場合などは、JSON to Goみたいなツールを使うと楽ができます。
Q: ポインタの記号がよくわかりません
*
と&
があって使い方がよくわかりません。
A: ポインタなのか、インスタンス(実体)なのかをまず区別することが大切です。
インスタンスはメモリ上に確保されているデータの本体です。100バイトの文字列であれば100バイトぶんのメモリを消費しています。一方、ポインタはインスタンスの場所情報です。64ビット機であれば8バイトです。
インスタンスがメモリ上にあれば、そのメモリのアドレスはかならず1つあるので、インスタンスからポインタを作ることができます。また、ポインタは特定のアドレスを指しているので、ポインタからインスタンスを取り出すこともできます。相互に変換できる、というのは大切な特性です。
&
は、インスタンスからポインタを取り出す操作です。下のコードのうち、下の方には&
がついています。これはインスタンスをメモリ上に作った後にポインタを取り出して変数に入れています。
// personIにはインスタンスが入る |
*
には2つの意味があります。1つはポインタから、インスタンスを取り出す「操作」です。&
の反対の操作です。
var name = "小椋佳" |
もう1つはポインタを表す型です。変数、引数の型につきます。
var person Person // personはPerson構造体のインスタンスが入る |
ポインタに関する記号にはもうひとつあります。それが.
です。構造体はインタフェースのメンバーへのアクセスで使いますが、この場合はポインタだろうがインスタンスだろうが気にしないで「使える」という特別な特性があります。C/C++の場合はポインタのメンバーにアクセスする場合は->
を使っていましたが、Goはどちらも.
でOKです。
Q: :=
と=
の使い分けがわかりません
代入に記号が2つありますが、いまいち使い分けでいつも悩んでしまいます。
A: 何が必要かはエディタが教えてくれます。エディタに従いましょう。
明示的に型を指定して変数を作りたい場合はvarを使って=
を使います。ここで:=
を書くとエラーになります。
// 右辺は文字列だけど、interface{}にしたい |
変数宣言なしで宣言と代入を同時にやろうとすると:=
を使います。ここで=
を書くとエラーになります。
既存の変数に代入するときは=
を使います。ここで:=
を書くとエラーになります。
間違ったらコンパイラが教えてくれますし、エディタやIDEも赤線をひいいてくれますので、悩む前に手を動かしてしまうのが楽です。
注意すべきは新しいスコープを作る場合です。次のコードはif文のところの記号は:=
でも=
でも動作します。ifの条件節は新しいスコープの中になるため、新しい変数を重複してもエラーになりません。また、親のスコープで同名の変数があれば、=
にしても動作します(ただし、親側の変数が書き換わる)。
package main |
Q: 返り値の宣言で変数名を入れるのはどういった意味がありますか?
返り値の宣言で変数名を入れる記法がありますが、横に長くなってメリットを感じません。どういったご利益がありますか?
A: メソッドの引数をn番目でアクセスしないで、名前でアクセスするのと同じで、返り値の意味を利用者や実装者に伝えるためのものです。
特に、データを外部から読み込みをするが、データ、行、列、エラーと返り値がたくさんある、みたいなケースで数が多くなってくるとプログラムのreturn文がわかりにくくなってきます。ドキュメントも読みやすくなります。
名前付きの返り値は関数開始時にゼロ値で初期化されますので、文字列の場合は空文字列、数値系の型はゼロ、bool型はfalse、errorなどのインタフェースやポインタはnilになります。エラーなんかは発生しなければそのままreturnすれば問題ありません。
Q: 関数の引数に型名が書かれていないものがあるのですが、どう解釈すればいいですか?
func Func(a, b, c []byte)
のような宣言がありました。aとbの型はなんでしょうか?
A: Goでは省略した引数は後ろに宣言した型が自動で入ります。
変数宣言で次のような宣言があった場合に違和感を感じる人はあまりいないのではないでしょうか? これが引数のところでも使える、と考えればOKです。もちろん、最後の変数に宣言がないとエラーになります。
var a, b int |
Q: immutableなコーディングがしたいのですがどうすればいいでしょうか?
最近のプログラミング言語では変更不可能であると宣言することで、デバッグなどがしやすくなっており、Goでもやりたいと思っています。
A: Goにはあまりimmutableに実現する手法はありません。諦めてください。
TypeScriptのconst
は再代入禁止なので、結構気軽に使えました。変数宣言をすべてconst
に揃えるという方法で機械的にimmutableスタイルに近づけます。Goのconst
は整数や文字列などのプリミティブには使えますが、スライス、配列、map、構造体のインスタンス、構造体のポインタなどには使えません。
また、map、スライスなどは一部の要素を変更するたびに全コピーというのは遅いためコードレビューで集中砲火を浴びることになるでしょう。
構造体のメソッドのレシーバをポインタではなくてインスタンスにすると、変更した内容がインスタンスには伝搬しなくなるため、予期せぬ変更を防げるぐらいの機能はあります(が、これも変更したつもりで変わっていないというわかりにくい挙動になるので注意)。
構文
Q: 三項演算子が使いたい
条件付きの初期化処理などで、三項演算子が使いたいです。
A: Goにはありませんので、if文を書いてください。
リテラル・スライス・map
Q: “sss” + 1とか、暗黙的型変換してくれない
他の言語だと、文字列と数値の結合とかをしても、適切に変換してくれます。Goの場合は文字列と数値の結合はおろか、整数と小数の計算もエラーになって不便です。
A: 暗黙の型変換は予想外のバグを産むことがあるため、すべて明示的に書くのがGoの考え方です。つまり、これは実現不可能です。
fmt.Sprintf
で文字列に変換する
結果が文字列であれば、fmt.Sprintf
を使ってあげるのが簡単です。%v
はどんな型でもそれなりに変換してくれます。細かい指定が必要であれば他のフラグを使って指定もできます。
fmt.Sprintf("%v%v", "abc", 1) |
文字列→数値、数値→文字列なら strconv
パッケージを使う
strconvパッケージには文字列と他のプリミティブ型の相互変換の関数がもろもろ定義されています。Format
で始まる関数は文字列への変換、Parse
で始まる関数は文字列から変換です。fmt.Sprintf
よりはコードは長くなりがちですが、こちらの方が高パフォーマンスが期待できます。
キャストで数値にする
数値型同士の変換はキャストします。誤差がどうなるかも考えて、実装者が責任を持って選ぶ必要があります。
price := 1000 |
Q: 配列を返す関数を実装するときに、空の配列のreturnが面倒ですが簡単に書けませんか?
例えば、検索してマッチした要素のインデックスの一覧を返す関数を作るとします。通信エラーの場合に空スライスとエラーを返したいのですが冗長になってしまいます。
func FindIndexes(name string) ([]int, error) { |
A: Goではnilが空スライスとして使えるようになっています。
append()
やforループにnilの空スライスを渡してもメモリアクセスエラーになったりせずに、空のスライスとして振る舞うようにGoのランタイムはデザインされています。そのため、初期化時はわざわざ空スライスを作ってあげる必要はありません。
var indexes []int // この段階では何も代入してないのでnil |
単なるnilでは型情報がないのでエラーになりますので、returnで型が決まっている場合以外は変数宣言は最低限必要です。
index = append(nil, 10) // first argument to append must be typed slice; have untyped nil |
なお、[]int{}
と初期化すると、空とはいえ実態が作られますので、nilと比較するとfalseになります。空スライスかどうかの判定はlen()を使いましょう。
indexes := []int{} |
Q: GoにはJavaやPythonやJavaScriptにあるSetがありませんが、どうするんでしょうか?
A: 一番簡単な(他への依存がない)方法はmapで代用する方法です。
例えば、キーが文字列であれば、map[string]bool
みたいにするのがもっとも簡単でしょう。
// 作成 |
もし、和集合とか積集合とか差集合が必要であればgithub.com/golang-collections/collectionsパッケージが使えるでしょう。
あるいは、順序の維持も必要であれば、ソート済み配列とsort.Searchなどを駆使する手もあります(楽ではありませんが)。ソート済み配列を対象にしたアルゴリズムのコードジェネレータもあります。
関数・ロジック
Q: クロージャって何ですか? 何がうれしいんですか?
クロージャという言葉をよく聞きます。クロージャとはなんですか? ループの変数でトラブルが起きやすいとも聞きます。使うとどのようなメリットがあるんでしょうか?
A: クロージャというのは、自分が作られた環境の外の変数への参照を保持した(閉じ込めた)関数で、主に2つの用途があります。
多くの言語では無名関数やラムダと呼ばれる文法を使って作られます。Goは無名関数(funcで作る名前のない関数)です。関数の中で関数を定義すると、その関数は当然外の変数にアクセスできて当然である、と誰しもが思うでしょう。しかし、これはコンパイラが気を利かせて、関数に隠れた引数を追加して、中からアクセスしている変数のポインタを渡すようにコードを改変しているのです(Pythonは親の名前空間として持っていて、ローカルで参照できない場合は親の名前空間に順番に探しに行くことで解決)。
たとえ、親の関数を抜けて、クロージャだけが存在する状態になっても、ローカル変数のnameが残り続け、あとからクロージャを実行してもその変数が残ります。
func Function() func() { |
要素に分けてこれから説明しますが、普段使うときはここまで考える必要はあまりないでしょう。大抵のイディオムの中で知らずに使っていることが多いです。
クロージャを使って遅延実行・コールバックをする
クロージャは定義されたタイミングと実行されるタイミングが少しずれます。何か実行の準備が整ったタイミング、何かイベントがあったタイミングでコールバックされます。
単なる関数を別に定義しても結果は同じですが、その定義したところのスコープの変数へのアクセスが保持されるため、引数リストを短くできます。
// スコープを抜けたタイミングで後から実行されるクロージャ |
クロージャの中に変数やデータを閉じ込める
乱暴な言い方をしてしまえば、構造体を作らずに、構造体のようなものを作ることです。Tour of Goのクロージャの説明はここにフォーカスしていましたね。
func fibonacci() func() int { |
引数の数が多かったり、呼び出し条件が複雑だったりする場合、なおかつ、定義場所と遠く離れたところで呼び出される場合(ただし、フレームワークにユーザー定義の振る舞いを設定するケース以外)は構造体とメソッドにしてもいいかもしれません。実行場所と定義場所が離れているクロージャの本体を探すよりは、構造体を使った方がIDEで定義を探すのは簡単です。
type fibonacci struct { |
ただ、Tour of Goのような無限配列のようなコードの場合はクロージャ単体よりも、チャネルとgoroutineを使った方が良いでしょう。なお、このコードは、サンプルをシンプルにするために無限ループになっており、外から中断できるようになっていないため、goroutineリークするコードになっていますので、このままコピーはしないでください。非同期処理・並列処理の書き方を参照してください。
func fibonacci() <-chan int { |
Q: タイムアウトのエラーと並列の重い処理をどう組み合わせていいかわかりません
タイムアウトがチャネルになっています。複数の処理の待ち合わせは sync.WaitGroup
を使いたいのですが、どのように組み立てれば良いかわかりません。
A: Goには、待ち合わせには関数呼び出しによるブロックと、チャネルと主に2種類あります。まずはすべてをチャネルに集約して、select
に持ち込むのがポイントです。
例えば、sync.WaitGroup
を使う場合は、終了した時にそれを通知するチャネルをあらかじめ作り、Wait()
完了後にそのチャネルに送信することで、関数呼び出しのブロックをチャネルに変換できます。
func NewWaitGroup(count int) (done func(), allDone <-chan struct{}) { |
チャネルどうしであればselect
が使え、どちらか先に解決したほうが実行される、ということが簡単に実現できます。
done, allDone := NewWaitGroup(1) |
なお、チャネルをラップしたデータ構造としてはcontext
があります。context
は終了通知用に特化したチャネルをラップして、関数間でやり取りをしやすくしたものです。
Q: コンテキストを受け取る関数ってどんなものなんでしょうか?
あと、コンテキストとはどのようなもので、どのように使えばいいのでしょうか?
A: コンテキストを受け取る関数は、処理に長い時間のかかる関数です。もし呼び出し側の都合で中断させたいときにcontext
を使います。時間がかかるという点では、他の言語で言う所のasync関数と同じようなものと言えます。
コンテキストは、goroutineを使って非同期に柔軟に仕事するGoで、まとめて中断したり、他の言語でいうスレッドローカルな、1つの処理単位に閉じた並列処理用の情報共有の手段としてGo 1.7から導入されました。
ウェブサービスは1つのHTTPリクエストを起点に処理がスタートしますが、その中で多数のAPIリクエストを並行して行ったりします。この「リクエスト単位の処理」を識別し、情報共有やら中断の通知をするために、context.Context
のインタフェースのインスタンスを共有します。他の言語だと、1リクエストは1スレッドとして、スレッドローカルなストレージを使ったりスレッドのIDを使ったりしますが、Goのスタイルの方が柔軟です。
書き方は次の3つに分けて説明します。
- 作って呼び出す側
- 外部プロセス起動、APIアクセスなどのOS由来の重い処理を投げる
- プロセス内の複数のタスク呼び出しの結果取得などの重い処理を扱う
まずは作って呼び出す側の書き方です。
コンテキストを受け取る関数はctx
という名前で第一引数で渡すのがGoの流儀です。
コンテキストはデフォルトのcontext.Background()
で作り、必要に応じて手動キャンセルがしたい(context.WithCancel()
)、一定時間経過したら中断したい(context.WithTimeout()
)、指定した時間になったら中断したい(context.Deadline()
)でラップします。
func handler(w http.ResponseWriter, r *http.Request) { |
次に外部プロセス起動、APIアクセスなどのOS由来の重い処理を投げる書き方の紹介です。標準ライブラリのnet/http
やos/exec
はcontext
を受け取って、終了通知が来たら通信を中断したり、外部コマンドにシグナルを送って中断させるキャンセル処理ができるAPIも提供しています。net/http
は多少不便なので、非標準のctxhttp
を使うと簡単ですし、そのうち標準ライブラリも改善される予定ではあります。
import ( |
最後に、自分でコンテキストの中断をハンドリングさせる方法です。コンテキストの中はチャネルですので、重い処理をすべてチャネルとして取り出せるようにしておけば、select
を使ってコンテキストの中断と一緒に扱うことができます。
func launchBatch(ctx context.Context) { |
Q: 省略可能な引数はどのように実現するのですか?
Pythonや最近のJavaScriptは引数を省略する、デフォルト値を設定する方法を提供しています。Javaはオーバーロードを駆使すれば変数の数によって似たことを実現できます。Goではどのようにするのでしょうか?
Goには省略可能な引数もオーバーロードもありません。可変長引数や、メンバーが省略可能な構造体を利用してオプション引数を実現します。
なお、可変長引数で型を interface{}
にして動的に型アサーションして読み込む方法もありますが、複雑になると破綻しがちなのと、引数の役割が呼び出し側でわかりにくいのでここでは紹介しません。
名前違いの関数をいくつも提供する
たとえば、strings
パッケージには、文字列の前後の指定された文字をカットするstrings.Trim(s, cutset)
関数と、文字列の前後のスペースを取り除くstrings.TrimSpace(s)
があります。
細かくたくさんの引数を受け取る関数を定義し、それをラップしてデフォルトの引数を付与する便利関数を定義する、というのがGoでよく利用される方法です。
オプション構造体
最近よく見るパターンです。実装が簡単なので実装者にとってはやりやすい方法です。
type Option struct { |
Builderパターンの変形
JavaでおなじみのBuilderパターンの変形もたまにGoでみかけます。Builderパターンは本来は構造体などの初期化で使うパターンですが、関数呼び出しでも使えます。
Google Drive APIでは大々的に使われています。List()の返り値はDrivesListCallという構造体で、この構造体のメソッドを呼ぶたびに、構造体に引数が設定されていきます。最後にDo()
を呼び出すと、処理が実行されます。それ以外にもORマッパーのクエリーの組み立てでも使われたりします。
難点としては、実装が多くなりがちなのと、このパターンを知らない人がいきなりドキュメントを見ても使い方が難しいと感じがちな点です。ただ、コード補完はバッチリ効くので、一度なれたら快適でしょう。
driveService, err := drive.NewService(ctx) |
Pythonの疑似コードで例えるなら次のような感じになります(実際のGoogle提供のAPIとは違いますが)。
list = drive_service.list(page_size=20, Q="name contains 'secret'") |
設定値をグローバルな構造体に設定して、それを利用する関数を使用
net/http
のhttp.Get()
などが利用しているパターンです。
HTTPアクセスには、アクセス経路(TCP/IPなのか、はたまたローカルのサーバーとUNIXドメインソケットで直結なのか)とか、タイムアウトとか、TLSの設定とか、パラメータが大量にあります。簡単関数とそれらを1つずつ受ける関数を作るのも大変ですし、ちょっと高度な使い方を使用としたときに呼び出し先をすべて変更してまわらないといけないのは大変です。
Goのnet/http
パッケージは、DefaultClient
というClient
構造体のインスタンスがグローバル変数として定義されており、http.Get()
などはこれのメソッドを間接的に呼び出します。
func Get(url string) (resp *Response, err error) { |
このDefaultClient
に、自作のClient
のインスタンスを入れることで、プログラム全体が同じ通信設定を利用できます。大抵、通信設定が複数に必要になるケースはあまりないため、このようなAPIでも問題なることはいまのところ聞いていません。
単なるグローバル変数と何が違うかというと、構造体単体を初期化してそのメソッドを呼ぶと、他の設定に依存せずに独立して利用できます。いざという時に複数の設定が必要になっても機械的に移行できますし、ライブラリ側のテストもしやすいです。
可変長引数を利用した方法
Goでは、すでに存在している型にも別名の型を定義でき、それを制約にできます。
Go PatternsのFunctional Optionsはその応用例の1つです。Go Patternsのサンプルの完成形の部分だけ貼ります。
fillerFile, err := file.New( |
難点はコードの量が多くなる、パッケージのドキュメントが散らかる(New関数も引数の関数もフラットにソートされてしまう)ために、読みにくくなるといったことが挙げられます。あまり現実世界では見たことがありません。
Q: goroutineの中から外の変数にアクセスすると値がおかしくなる
ループの中でループ変数などを参照するときになぜか変な値になってしまいます
for i := 0; i < 10; i++ { |
A: 関数は定義されている外の情報にアクセスできます。ただし、ポインタを持っているだけなので、値が変更されると呼ばれたときではなく、最新の値を読み込んでしまいます。
たいていのプログラミング言語では、「レキシカルスコープ」と呼ぶこの機能を実装しています。関数の中で関数を定義すると、その関数は当然外の変数にアクセスできて当然である、と誰しもが思うでしょう。しかし、これはコンパイラが気を利かせて、関数に隠れた引数を追加して、中からアクセスしている変数のポインタを渡すようにコードを改変しているのです(Pythonは親の名前空間として持っていて、ローカルで参照できない場合は親の名前空間に順番に探しに行くことで解決)。
Goのgroutineは高速とはいえ、forループが回る速度よりは低速です。そのため、goroutineが起動するころにはほとんどループが終わってしまっています。そのため、ほとんどの上記のgroutineではiが10になっています。
ポインタなのが問題なので、インスタンス化してコピーを保持するのがもっとも安全な解決方法です。コピーは関数の引数として渡す方がよいでしょう。これにより、goroutineが起動したときの変数の状態を固定化して、期待通りの結果が得られます。
for i := 0; i < 10; i++ { |
Q: deferの中で変数の値が期待と違います
deferは終了時に呼ばれるはずですが、変数が終了していない状態のものになってしまっています。
finished := false |
A: 呼び出しはスコープを抜ける時に行われますが、引数の評価は宣言時に行われています。
クロージャの時のケースの逆です。このケースではdefer文のところでfinishedのインスタンスのコピーが作られて固定化されてしまっているため、その後finishedを変更してもdeferの中では呼び出し時の状態に固定化されてしまっています。関数終了時の状態が必要であれば、クロージャにする、ポインタを引数で受け取るなどして最終状態にアクセスできるようにしなければなりません。
finished := false |
Q: 関数型スタイルのリスト処理がやりたいがGoではどうすれば良いですか?
JavaScriptのArrayのmap/reduce/forEach/filterのメソッドを利用したコーディングが好きです。Javaにもstreamが入りました。Pythonにはリスト内包表記があります。Goではどのようにすればいいでしょうか?
A: Goではそのスタイルをサポートする機能があまりないのであきらめてください。
手続き型的にコードを書くのではなく、リストに入ったデータに対して、その加工方法(関数)を渡して(高階関数)すべての要素にパイプライン的に処理させる機能を充実させている言語は増えていますが、Goではそのようなサポートはありません。あきらめてforループを書きましょう。もちろん、末尾再帰もありません。パターンマッチもありません。
関数が一級オブジェクトではあるので、自分でmap相当の処理を書くことで似たことは実現できますが、あまり強力に推論してくれたりしないので、型アサーションの嵐になるか、リフレクションで頑張らざるを得ないため、必ずしもシンプルには実現できないでしょう。
Q: 外部のAPI呼び出しなどの時間のかかる処理でタイムアウトを実装するにはどうすれば良いでしょうか?
構造体・インタフェース
Q: Goの構造体とかインタフェースの定義の構文は冗長に見えます
Javaとかだと、class { 実装 }だけど、キーワードが多いし順番に違和感を覚えます。
A: Goは構造体もインタフェースもインラインで定義できます。インラインの定義構文+名前の定義の組み合わせになっています。
例えば、テーブル駆動テストはたいてい、インラインで構造体を定義してその配列をその場でインスタンスまで作ってしまいます。1行でやってしまっています。関数内部でしか使われない型はこのようにつくってしまえるのがGoです。
func TestSum(t *testing.T) { |
一方、既存の型に名前をつけるのがtypeです。type 新しい名前 既存の型
で使います。
type ErrorFlag int |
Goの構造体定義の書き方はこの2つの合成です。既存の型部分にインラインの構造体定義がくっつているというわけです。
type SmallAnimal struct { |
インタフェースのインライン定義も使ったことはありませんが、osパッケージのerror.goで見ることができます。
e, ok := err.(interface{ Is(error) bool }) |
この一行で、errというポインタ変数がfunc Is(error) bool
というメソッドを持っているかどうか、という判断ができます。
Q: 構造体をJSONに書き出そうとしているのですがメンバー変数の値が出力されません。なぜでしょうか?
type Person struct { |
A: 外部パッケージから利用できる名前は大文字スタートでないといけません。encoding/json
パッケージはリフレクションでデータを読みに行きますが、大文字スタート以外の名前の変数は無視されます。
そのため、name, ageの先頭を大文字にすれば出力されるようになります。
Q: 汎用的なロジックを実装したいが、Goの場合は型が決まってしまうため再利用がしにくい
複数のデータ変換用の構造体に対する処理とかを書くのが大変。どうすれば良いか?
A: 共通化を行うためには、共通化のための仕組みを作り込む必要があります。
Goはなるべく高速に動作し、型のチェックをきちんと行いつつ、すばやくコンパイルが完了するというのを目指して作られた処理系です。柔軟性よりも、存在しないメンバーや変数へのアクセスがないかがすぐにわかって、実行時のメモリレイアウトがカチッと決まることがGoのコンパイラの価値です。
共通化のための仕組みを実現する方法はいくつかあります。
インタフェースを実装する方法
処理対象の構造体の共通インタフェースを定義して、それに対する処理を書きます。インタフェースは構造体のメンバー変数へのアクセスができないため、まず、必要な読み書きのメソッドを用意します。その後、そのメソッドを持つインタフェースを定義して、共通処理をそのメソッドを使って行うようにします。
type Person struct { |
構造体の埋め込みを使う方法
共通の属性が一意に定まり、なおかつ共通処理はそこの情報にしか絶対にアクセスしないと保証できるなら、共通の属性を構造体として切り出し、それを各構造体に埋め込みます。
ただし、共通処理に対するコードを書く場合は、それ以外の要素にアクセスしようとか、メソッドのオーバーライドをしたいとか、一般的なオブジェクト指向言語のノリで扱うと想定通りに動かなくて時間を取られることになりますので、用法容量を守ってお使いください。
type Living struct { |
Q: 構造体にメソッドを追加しました。メンバー変数を書き換えようとしても変更されません。なぜでしょうか?
type SmallAnimal struct { |
A: メソッドのレシーバーがポインタでないとメンバーへの変更ができません。
読み込み専用にしたい場合はポインタを外してインスタンスにします。基本的には最初は全部ポインタをつけておいて、「このメソッド内部では変更しないな」というのが確定する場合だけ外すというのが良いでしょう。メソッドからメソッドを呼ぶ場合がややこしいので、困ったら全部ポインタにしてしまいましょう。
Q: ある構造体に対する処理を実装する場合は、メソッドにすべきか、それとも構造体を引数に取る関数にすべきか?
Goには構造体の処理の書き方が2通りあります。どちらが良いですか?
// 1: メソッド |
A: 最初にメソッドで実装してしまえばいいんじゃないでしょうか?
メソッドの方は、使う側からすれば、 s.
とドットを打った時点でそれに関連するメソッドが補完されますので、再認でコーディングがしやすいというメリットがあります。また、関数にしても、パッケージ名.関数名
となってしまうため、利用するコード上は多少冗長になります。
ただし、処理対象が1つの構造体ではなく、複数の構造体がくる可能性のあるインタフェースになる場合は、関数の方が1つの実装でたくさんの処理対象に対して利用できるので、数が多くなってきたら関数で良いかと思います。
Q: エラーの構造体を作っています。そのエラーがタイムアウトかそうじゃないかを機械的にboolで判断できるようにするメソッドを追加したいのですが、どうすれば良いでしょうか?
標準のエラー構造体は文字列を取り出すError()メソッドしかなく、詳細情報をそこをパースして取り出すのは変更に弱いコードになってしまいます。良い方法はないでしょうか?
A: errorインタフェースを満たす実装以外に別のインタフェースも提供して、型アサーションで別のインタフェースを取り出す方法があります。
まず、公開要素のTimeoutErrorインタフェースと、非公開のtimeoutError構造体を作ります。
type TimeoutError interface { |
何かしらの処理がタイムアウトしたときは、timeoutError構造体のポインタ値を返すようにします。これはerrorインタフェースを満たすので、関数の返り値はerror型でOKです。
で、この構造体はerror型以外にも、新規で作ったTimeoutErrorインタフェースも満たしますので、インタフェースからインタフェースの型アサーションも成功します。そのため、次のようにキャストすることでerrorインタフェースが持てない情報を別のインタフェースを通じて提供できます。
if _, ok := err.(TimeoutError); ok { |
一定以上の年齢の人にはCOMのqueryInterfaceと言えば伝わるテクニックです。
エラー処理・例外処理
Q: エラーの種別はどのようにして区別すれば良いでしょうか?
nil
と比較することでエラーの有無の確認はわかったのですが、タイムアウトなのかファイルがないのかを区別するにはどうすれば良いでしょうか?
A: ここはGo言語の実装者同士の間でも設計がぶれているところですが、基本的には型アサーションで行うことが多いでしょう。
Go本体のコードを見ても、2種類あります。osパッケージにはエラー種別を区別する関数がいくつか提供されています。IsExists, IsNotExist, IsPermission, IsTimeoutがあります。この方式はコードが一見きれいに見えますが、osパッケージ以外では見ない気がします。
_, err := os.Stats("file") |
他のケースでは型アサーションがあります。こちらのほうがサードパーティ製ライブラリでは一般的な気がします。
err := json.Unmarshal(jsonStr, person) |
Q: panicはどのような時に使うのか?
Goは例外がない言語で、errorを最後の返り値として渡すのが一般的ですが、panicはどうやって使うんでしょうか?
A: panicとrecoverを使えば例外のようなことができますが、Goでは一般的ではありません。プログラマーの間違いを引っ掛けるのに使う、と考えれば良いでしょう。
Goでよくpanicを使うのは、入力値が固定値だったり、実行時に変動しないデータを扱う場合です。例えば正規表現、テンプレートなど、入力値が文字列で、内部でコンパイルのようなことを行う関数は、Must
で始まる関数も提供しています。このMust
な関数は処理が失敗するとpanic
になります。
var validID *regexp.Regexp |
Q: log.Fatal()
や os.Exit()
ってどのような時に使うのか?
これらの関数を使うとプログラムが終了できますが、いろいろ副作用があるようです。どこで使うべきですか?
log.Fatal()
は内部では os.Exit(1)
を呼んでいるので、 os.Exit()
ど同等なので後者に絞って説明します。 os.Exit()
を呼ぶと、その次の行が実行されずにプログラムが終了します。問題なのは、 defer
で設定した後処理が無視されてしまうので、完了時にネットワーク切断とかもろもろ後片付けをする行儀の良いコードが動作しなくなります。また、そのロジックが有用なコードでも、コマンドラインツールのように1回実行して完了するプログラム以外で使用が不可能になります。
基本的には、main関数以外ではerrorを上流に返していき、最後の最後、main関数の中で os.Exit
を呼んでステータスコードを0以外にする、という使い方以外で使うことはないでしょう。
Q: 並列処理で複数のAPI呼び出しをしています。どこかでエラーがおきた時にまとめて終了させたいときはどうすればいいですか?
それぞれの処理で途中で継続できなくなった時に、他の処理も中断させたいと思います。どのようにすれば良いでしょうか?
A: context
はまさにその用途で使うものです。context
は並列処理で使える例外のようなものです。
JavaやPythonやJavaScriptはエラー処理機構として例外を持っており、Goはそれを持っていないと言われます。しかし、一般的な言語の例外は、呼び出し先から呼び出し元に戻っていきます。その途中で受け取って後片付けを行ったりしますが、呼び出し元と呼び出し先は1:1の関係です。Goのようにgoroutineをカジュアルにたくさん作って処理を行う場合。どこかで復旧不能なエラーが発生したら、並行で実行されている他のタスクもキャンセルしたいですよね? そのような場合にcontext
を使います。
ただし、context
パッケージをそのまま使い、タスクの終了を監視しつつ(sync.WaitGroup
)、各ジョブのエラーのレスポンスを監視し、どこかのgoroutineがエラーを返したらcontext
のキャンセルを行うというコードを書くのは結構骨が折れます。非標準パッケージのgolang.org/x/sync/errgroup
を使うと、1/10ぐらいの行数で実現ができます。
Go()
メソッドはerrorを返す関数で、これがerrorを返すと、すべての並列実行タスクを終了します。これは並列じゃなくてもよくて、順次実行されるジョブでも使えます。
package main |
ロギング
Q: ログレベルを設定したログ出力はどのように実現するのか?
JavaのLog4J、Pythonのloggingパッケージではinfo/warnのようなログの出し分けができますが? Goではどのようにすればいいですか?
A: Goの標準ライブラリではサポートしていません。logrusやzapなどのサードパーティ製のロギングライブラリを使うのが良いでしょう
以前はlogrusほぼ一強でしたが、ハイパフォーマンスをうたったzapの人気も高まっています。zapは構造化ログに特化していますので用途に応じて選ぶと良いでしょう。zapはサンプリングレートなども設定できるので、本番環境でログが多すぎて困る、というケースでとりあえず対処するのも簡単です。
Q: ログがChromeに比べて見にくいです
A: Chromeの開発者コンソールはたいていのプログラミング言語のデバッグ環境よりも圧倒的に良いので諦めましょう。
データベース
Q: DBのトランザクション制御はdatabase/sql
のBegin()
つかう?
直接間接問わず、最終的にはこのメソッドでトランザクションを制御することになるでしょう。
sqlxも内部ではBegin()
を使っていますし、gormもsqlパッケージのBeginTx()
を使っています。便利ライブラリを使っても最終的にはdatabase/sql
にたどり着きます。
まとめ
何か困ったことはないですか? と自社のチャットやら某コミュニティに投げて飛んで来た質問とか困った事例とかについては一通り回答を書いたりした、というのが今の状況です。コンパイラでエラーになるものはここでは入れてはなくて、文法を学んで、じゃあそれを組み合わせてどう使おう、というものだけにひとまず限定しています。
もちろん、これをまとめたあとにもいくつか質問が飛んだりしていて、入れたいものはたくさんありますが、ウェブで技術ブログという体裁で出すのは分量的にこれ以上は厳しいかなぁ、という気もしますので、今後どうやってまとめていくかはまた考えたいと思います。
参考文献とか他のおすすめ
- http://tmrts.com/go-patterns/
- https://medium.com/eureka-engineering/go-beginner-3bb95e0790da
- https://qiita.com/shibukawa/items/16acb36e94cfe3b02aa1: 昔書いたオブジェクト指向なプログラミング言語のユーザー観点での記事