ここ最近Go言語(Golang)を触っていたので、ちょっとした感想記事を。
少し触ってみたのですが、ぼやっと書き方?みたいなものが分かってきたような?...というくらいです。
最大3ファイル程度で終わる小規模なコードのみ制作経験。さらに複数人での開発経験もないので、浅い感想記事としてお楽しみください。
興味を持ったきっかけ
興味を持ったきっかけは以下です。
- 静的型付け言語
- コンパイル言語。しかもコンパイル後はexe形式に。
- 明確なオブジェクト指向ではない
1、2は筆者がJavaメインのため相性が良かったです。3はネットで明確なオブジェクト指向ではないとあり、どのような考え方なのか勉強してみたいなーと思いました。
特徴的と感じたところ
クラスが存在しない
Go言語はクラスが存在しないです。以下のようにクラスっぽいものは作成することは出来ます。
type RecordBuffer struct {
records []Record
}
func (buf *RecordBuffer) Append(record Record) (uint64, error) {
record.Offset = uint64(len(buf.records))
buf.records = append(buf.records, record)
return record.Offset, nil
}
func (buf *RecordBuffer) Read(offset uint64) (Record, error) {
if offset >= uint64(len(buf.records)) {
return Record{}, ErrOffsetNotFound
}
return buf.records[offset], nil
}
クラスは状態と手続きを1つにまとめたものを指しますが、Goではこのように状態を保持する「構造体」と手続きの「メソッド」を同じブロック内に書きません。データを保持する「構造体」と、構造体への操作手順である「メソッド」を分離させています。
クラスの場合は、クラスを定義する場合はまず開発したいものに対して、そのクラスに負わせる責務を考えます。そして、責務を果たすために外部に公開する操作(メソッド)を考え、メソッドが正しく動作するのに必要な状態(メンバ変数)を考えます。
メソッド→メンバ変数 の順番
それに対してGoの構造体は、操作(メソッド)は別で定義するため、開発したいもののデータ構造をそのまま構造体として定義させたがっているように感じました。そしてそのデータ構造に対する不可分な処理をメソッドとして定義するイメージです。
メンバ変数→メソッド の順番
継承を廃して、ポリモーフィズムは残す
Go言語にはクラスがないので継承の概念はありませんが、上記のメソッドとinterfaceを組み合わせてポリモーフィズムを実現出来ます。ポリモーフィズムは異なるクラスを1つの抽象型で纏めて、同じメソッド呼び出しで異なる動作をさせることですね。
継承はよく問題視されます。サブクラスは暗黙的にスーパークラスのメンバを保持しているため、サブクラス利用者はサブクラスのクラス定義に書かれていないスーパークラスの内容を把握している必要があるためです。
この問題視されがちな継承を廃して、ポリモーフィズムを残すことでオブジェクト指向的な部品化が出来るようになっています。
「抽象に依存する」が言語仕様で自然と守られる
依存関係逆転の法則でよく言う「具象に依存するな。抽象に依存せよ。」の話です。
Go言語は「ダックタイピング」であり、また言語仕様として「パッケージが循環参照することを禁止」しています。これにより「使う側のパッケージ」にinterfaceが、「使われる側のパッケージ」に具象型を宣言することになります。
自然と依存関係逆転の法則が守られていますね。
触ってみて
そこまでオブジェクト指向的でない?
こちらでJavaでオブジェクト指向的にビンゴを責務分けしています。こちらのコードでは、「ビンゴか判定をする」責務を持ったビンゴ判定機能クラスを作成しています。
Go言語的に書くと、このビンゴ判定機能は構造体+メソッドでクラスっぽいものを作成しない気がしています。Go的にはビンゴ判定はただの手続きになるので関数(メソッドではない)で宣言する気がします。
このようにGoは全てをクラスとして表現するオブジェクト指向ではなく、基本は手続的に書く気がします。しかし以下のような場合に、クラスっぽいものを作成する気がしています。
- データ構造が明確な場合。データ構造通りに構造体を定義、不可分処理をメソッドに。
- 使う側クラスが具体的な実装を気にしていない場合。interfaceを自パッケージに、具象型を別パッケージに作成。
1がオブジェクト指向的なのかは微妙な気がしています。開発物のデータ構造をそのまま構造体と定義しているだけです。不可分処理をメソッドとして持つので、データの整合性保持の責務は持たせていますが、それ以外の「ビンゴか判定する」のような責務を持たせようがありません。1は部品化していますが、これだけではオブジェクト指向的に書けない気がしています。
2は責務を自由にinterfaceの公開メソッドとして定義できるのでオブジェクト指向的に書くことが可能になります。ここがGo言語のオブジェクト指向的な箇所に感じています。
基本は1のデータ構造に対する手続きを関数(メソッドでない)で作成する手続的なコードで、必要な箇所だけを2で部品化するイメージです。
少し触っただけの感想なので、慣れると全然違う感想を抱くかもです。
Javaと比較して
筆者はJavaのプログラマーなのですが、少し触った段階では以下のように感じています。
Java:既存コードを全く知らない不特定多数がコードを触ることを想定。(大規模向け)
Go:ある程度コードを触る人が既存コードを読んでいること想定。(小・中規模向け)
Javaは1ファイル1クラスで部品化(クラス化)しています。1ファイル内には、切り出された責務の内容しか書いていないので他のことは考えなくて済みます。また継承関係が宣言的なので、親インターフェイスを簡単に辿れどのように部品化されているのかがすぐに分かります。
そのため既存コードを知らなくても、全てがクラスとして部品化されているので、クラスの責務に着目すれば、処理の全体の流れはすぐに追えます。例のビンゴゲームもbingoGameメソッドを見ればどんな流れかすぐ分かると思います。
Goも部品化できますが、全てが部品化されていません。また、Goの部品化はクラスのように1ブロックとして一箇所に集まらず、飛び飛びに書かれます。さらにダックタインピングのため、具象型からは自分がどのような責務を負わされているのか確認できません。
私は経験が浅いのもありますが、Goは見たこともないコードを読むときはJavaよりも時間がかかる気がしています。
しかし、Goは書く時はすごく楽に書ける言語に感じています。順々に手続的に書いていって必要な箇所だけinterface定義で部品化を行うのです。一番難しい責務分けで頭を悩ませることがないので、書くスピードは圧倒的にGoが早い気がしています。
そのためJavaと比べて、Goは見ず知らずの人がコードを修正するような大規模案件ではなく、ある程度コードを知っているであろう中規模や小規模で良い選択ではないでしょうか。
最後に
個人的にGo言語の型機能、パッケージ、実行環境は気に入っています。前まではちょっとしたコード用にPythonだったのですが、その役目をGoに移そうと思います。
このまま使い続けて他人のコードをすんなり読めるようになりたいです。。。
コメント