はじめに
Java中心で仕事をしていた人がGoを書いてみると様々な文化の違いにぶつかると思います。そもそもオブジェクト指向言語と手続き型言語であるなど根本的に異なる点は多いのですが、「よりシンプルに」をモットーとするGoにはよりJavaエンジニアを困惑させる文化がたくさんあるように見受けられます。
本記事ではJavaエンジニアがGoを書いた際に感じた独特な文化を経験ベースで書いてみます。Goをこれから書くであろうJavaエンジニアのお役に立てれば幸いです。
対象読者
- これからGoを書くことになりそうなJavaエンジニア
- Javaと比べたGo特有の文化を知りたい人
- 実際にGoの開発をするにあたって役に立つ思想を知りたい人
本記事の対象でない人
- Goの構文や書き方を基本から学びたい人
- とりあえずプロダクトを作って動かしてみたい人
- 実際に開発する上で意識しない裏の仕組みやアーキテクチャを知りたい人
インタフェースの実装を明示的に指定しない
GoではJavaのimplements
のような宣言がありません。
インタフェースで定義したメソッドを持つ型を暗黙的にインタフェースを実装しているとみなします。
// ポケモンのインターフェース
type Pokemon interface {
Attack() string
}
// ピカチュウの構造体
type Pikachu struct {
Height int // 体長
}
func (p Pikachu) Attack() string { // Pikachu構造体はPokemonインタフェースを暗黙的に実装する
return "10万ボルト"
}
上の例では、Pikachu
構造体はPokemon
インタフェースのAttack()
メソッドを持つので、暗黙的にPokemon
インタフェースを実装しています。インタフェースの定義と構造体のメソッド定義が離れているとインタフェースを実装していることが分かりにくい場合もあるので注意です。
インタフェースを実装しているかどうかは以下の形式のコードを書いてコンパイルエラーが起こるかどうかで判定できます。
var _ Pokemon = Pikachu{} // PikachuがPokemonを実装しているか確認
var _ Pokemon = (*Pikachu)(nil) // *PikachuがPokemonを実装しているか確認
極端に短い変数名
Javaで省略した変数名を使用すると「意味のある変数名を付けろ」「命名をサボるな」とレビューでボコボコにされること請け合いですが、Goでは平気で1文字の変数名を使用します。
p := NewPikachu()
p.Attack()
ただし、無条件に短い変数名を使用してよいわけではないので注意です。変数の宣言と使用箇所が離れている場合やグローバル変数の場合は説明的な変数名を用いることが推奨されています。
pikachu := NewPikachu() // 使用箇所と離れているならより説明的な変数名をつける
// すごく長い処理
pikachu.Attack()
コードを読みやすくするという目的に沿って都度考える必要があります。
頭文字の大文字小文字でpublicとprivateを表す
Javaではクラス名はPascalCase、変数名メソッド名はcamelCaseのように項目ごとの命名規則が決まっています。
一方Goでは項目ごとに命名規則が決まっているのではなく、PascalCaseはpublicな項目、camelCaseはprivateな項目といった使い分けとなっています。
type Pikachu struct { // 構造体名がPascalCaseなためpublic
Nickname string // 変数名がPascalCaseなためpublic
Height int // 変数名がPascalCaseなためpublic
hp int // 変数名がcamelCaseなためprivate
}
privateな項目は他パッケージから参照することができません。Javaに慣れているとGoの命名規則をつかめなかったりパッケージ間参照に戸惑ったりするので注意です。
例外を使用しないエラーハンドリング
Goには例外の概念がありません。Goでエラーと言えばerror
インタフェースを実装した文字列値を持つ任意の変数を指します。
error
の定義は以下です。
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
関数内でエラーが発生しうる場合は返り値としてerror
を返し、ハンドリングしたい階層までエラー情報を伝搬させます。
result, err := pikachu.DoSomething() // 返り値にerrorを返す
if err != nil {
log.Fatal(err)
}
Javaの例外処理と似た動きを実現する仕組み(panic, recover)はありますが、気軽に使用して良いものではありません。
try {
// 例外が発生する可能性のある処理
} catch(Exception e) {
// 例外が発生した場合の処理
}
例えば上記のJava例外処理と同じような挙動をGoでは以下のような書き方で実現できます。
defer func() {
if rec := recover(); rec != nil {
// panicが発生した場合の処理
}
}()
// panicが発生する可能性のある処理
Go公式は単純なエラー処理や外部にエラーを伝える場合はerror
を、致命的なほど例外的な状況や内部的なエラー処理にはpanic
, recover
の機構を用いることとしています。正しく使用すればクリーンでシンプルなエラー処理を書くことができるようです。
Javaの検査例外・非検査例外と比べてそれぞれメリットデメリットあると思いますが、その方式に至った背景や思想を知ることが重要そうです。
標準のテストパッケージにアサートがない
Goのテストは以下のように書きます。
func TestNewPikachu(t *testing.T) {
pikachu := NewPikachu()
if pikachu == nil {
t.Fatal("Failed to create pikachu.")
}
if pikachu.Nickname != "pika" {
t.Errorf("pikachu.Nickname => %s, wants %s", pikachu.Nickname, "pika")
}
if pikachu.Height < 0 {
t.Errorf("pikachu.Height => %v, must not be negative", pikachu.Height)
}
}
Goのテストでは標準のtesting
パッケージを用いるのがデファクトスタンダードですが、testing
にはassert文らしきものがありません。代わりに、if
文で条件分岐させ、エラー原因を書きます。
以下のようなGoのテストの考え方が反映されているようです。
- assertは便利だが、頼りすぎるとエラーレポートが疎かになる
- エラーレポートの重要性を保つため、標準ではassertを用意しない
- 必要な機能は大体Go本体に揃っている
とはいってもassertは便利なので使いたいと思う方も多いようで、OSSにはアサート用のライブラリが転がっています。公式の考えに従うか、便利さをとるかはプロジェクトによって考えなければいけなさそうです。
フラットなディレクトリ構造
Javaで綺麗にプロジェクトを整理しようとするとより細かい粒度でパッケージを分割する傾向があります。体感的にですが、1パッケージに配置するjavaファイルは10を超えたら多く感じます。
user
├ domain
| ├ entity //各パッケージには数ファイルのみ配置
| ├ repository
| └ value
├ facade
├ store
├
...
一方、Goではかなり多くのファイルを同一階層ディレクトリに置いても良い文化のようです。
標準パッケージでいうと例えば、osパッケージは1階層に100以上のgoファイルを配置しています。
awesome-goに載るような評価の高いOSSでも、トップディレクトリにほとんどのファイルを置くようなフラットな構造のプロジェクトが多いです。
かなりの数のファイルをフラットに配置することを許容する文化なので、プロジェクトの立ち上げ時はトップディレクトリにファイルをフラットに配置する最小構成から始め、プロジェクト規模が大きくなっていくにつれて適切にパッケージを分割していく方針が良いのかもしれません。
非常にシンプルなパッケージ名
Goの標準パッケージは非常に充実していますが、一覧を眺めてみるとどれも1単語のシンプルなパッケージ名であることが分かります。
Goのパッケージ名は以下の方針に従って命名するべきと公式も述べています。
- 短くて明確なパッケージ名を付けるべき
- 小文字限定で、スネークケースやキャメルケースを使った文字区切りは非推奨
- 省略形は使っても良いが慎重に。プログラマに馴染み深い形なら使ってよい
- 標準パッケージの例:system call→
syscall
パッケージ、format→fmt
パッケージ
- 標準パッケージの例:system call→
- ユーザから良い名前を盗まない。一般的に使用される名前をパッケージには付けないこと
- 標準パッケージの例:
buf
は一般的にバッファの変数名としてよく使われるのでバッファI/Oパッケージ名はbufio
としている等
- 標準パッケージの例:
1単語かつ被らないような名前はなかなかハードルが高いですが、ここは開発者のセンスの見せどころですね。
おわりに
Goは開発者の思想を強く反映している言語なので、本記事に書いているような独特な文化には必ず理由がありそうです。
Goの文化を知るには公式ブログを読んだりOSSのコードを実際に読んだりするのが早いと思います。
OSSを探すならawesome-goがオススメです。独自の評価基準で優れていると判定されたOSSが一覧で見れます。自分が興味ある分野のOSSを探して中身を探索すると、Goとより親密になれるかもしれません。