はじめに
30歳、PHPとLaravelを数年書いてきたWeb系のバックエンドエンジニアです。
普段はフレームワークが敷いてくれたレールの上で楽しくプログラムを書いたり、新人教育的なことをしています。
2ヶ月ほど前から、自己学習としてGoを学び始めました。
正直なところ、「文法が違うくらいでしょ」となめていました。
でも今振り返ると、まず学ぶべきだったのは文法ではなく設計思想の方でした。
| 項目 | 特徴 |
|---|---|
| Laravel | 暗黙という魔法によって、記述量が減る |
| Go | 暗黙を嫌い、明示を強制する |
この価値観の差に、私は何度もつまずきました。
この記事では、これからGoを学ぼうとしている方に向けて
文法の前に設計思想を知ることの大切さ
をお伝えできればと思います。(Goだけに限った話ではないですが)
フレームワークと言語を比較するのはどうかと思ったのですが、私がイメージしやすいLaravelを比較対象としております。
私もまだまだ学習中の身ですが、少しでも皆さまの効率的な学習の助けになれば幸いです。
(間違っている認識などがありましたら、ご指摘いただけると嬉しいです。)
また、極力設計思想の部分にフォーカスしたいので、関数にレシーバをつけたらメソッドになるなど細かい文法は割愛しています。
つまずき①:クラスがない…!?
私はずっとクラスを書いてきたので、もはや体にクラスが染み付いています。
匂いがとれません。
そんな状態から学習をスタートしたので、さっそく次のような流れで壁にぶつかりました。
- クラスがないんだー
- パッケージや構造体があるんだー
- はい…?(途方もない壁)
なんとなく「パッケージと構造体」があるのはわかったのですが、「じゃあ何から書けばいいんだ」と固まってしまいました。
これ見てやっとけの代表格のA Tour of Goを一通りやってみたり、Udemyもやってみたのですが、結局「アプリを作りたい場合は何から書けばいいんだ...」は解決しませんでした。
そして、AIを使ったり調べたりして試行回数を増やしていくと根本的な違いに気づきました。
Goは、クラスでやっていたことを別の道具に分担させているだけだったのです。
「クラス」は、プロパティとメソッドを一つにまとめていましたが、Goはそれをこれから見ていく「パッケージ」「構造体」に役割分担させていました。
パッケージ = ディレクトリ という単位
Laravelだと、私はずっと「1ファイル = 1クラス」で考えていました。
App\Models\UserApp\Services\UserService
ところがGoは、**「1ディレクトリ = 1パッケージ」**が基本単位です。
同じディレクトリ内のファイルは、ファイルが分かれていても全部「同じパッケージの仲間」として扱われます。
たとえば「ユーザー一覧機能」を作るなら、userディレクトリ(=パッケージ)に、関連するファイルをまとめていくイメージです。
.
├── main.go
└── user
├── user.go
├── repository.go
├── service.go
└── handler.go
user ディレクトリ内のファイルは分かれていても、全部「user パッケージの仲間」です。
Laravelだと、以下のように書く役割ごとにディレクトリを分けて、その中に各機能のクラスを作成していました。
- Models
- Services
- Http/Controllers
Goでもこの分け方はできますが、馴染みやすかったのは、userディレクトリ(=パッケージ)ごとにまとめ、その中に役割別のファイルを並べる形でした。
つまり、1ディレクトリ内のファイル等で、一つのパッケージ(=機能)を作り上げるんだという漠然としたイメージが持てました。
構造体ってなんですか
パッケージは、超絶漠然とイメージが持てました。
では、その中に書く構造体とはなにか。
クラスは「データ(プロパティ)」と「動き(メソッド)」を軸にプログラムを書いていきます。
一方Goの構造体は、あくまで「データの集まり」を定義するだけのものです。
// 「データの形」を定義するだけ
type User struct {
ID int
Name string
}
「じゃあ振る舞いはどこに書くんだ」というと、ここは変わらずメソッドです。
// レシーバを指定してメソッドを書く
func (u User) Hello() string {
return "Hello, " + u.Name
}
func (u User)部分をレシーバと呼び、「このメソッドは構造体Userのもの」と紐づけている状態。
レシーバで構造体と紐づけることで、この動きはなんのデータを元にしているかを定義しています。
クラスのように「{}」の中に閉じ込めるのではなく、データの定義と振る舞いの定義が物理的に分かれているだけでした。
ここでもう一つ、クラスに慣れた身として戸惑ったのが、アクセス制御です。
PHPでは、以下のアクセス修飾子があり、これらでプロパティやメソッドの公開範囲を指定していました。
- public
- protected
- private
Goにはアクセス修飾子がない代わりに、以下のようなものを定義する際の頭文字が、大文字か小文字かで公開範囲を指定します。
- 変数
- 定数
- 関数
- メソッド
- 構造体のフィールド
大文字始まりならパッケージの外に公開(public)、小文字始まりならパッケージの中だけ(private)。
たとえば先ほどの user パッケージで、それぞれを定義するとこうなります。
package user
const MaxLength = 50 // 定数:公開
const tableName = "users" // 定数:非公開
var DefaultText = "member" // 変数:公開
var defaultNum = 3 // 変数:非公開
// 構造体:公開
type User struct {
ID int // フィールド:公開
Name string // フィールド:公開
age int // フィールド:非公開
}
// 関数:公開
func List() ([]User, error) {
return fetchAll()
}
// 関数:非公開
func fetchAll() ([]User, error) {
// 処理
}
// メソッド:公開
func (u User) Hello() string {
return "Hello, " + u.Name
}
// メソッド:非公開
func (u User) validate() error {
// 処理
}
以下のようなディレクトリ構造があった場合、小文字の識別子はmain.goからはアクセスできないということになります。
.
├── main.go // user パッケージの「外」
└── user
├── user.go // package user
├── repository.go // package user
├── service.go // package user
└── handler.go // package user
つまずき②:try〜catch がない…!?
PHPには try〜catch があります(今思うと、なかなか便利なやつでした)が、Goにはありません。
代わりに、エラーが返ってくる箇所で毎回以下のような記述が必要になります。
if err != nil {
// 処理
}
最初は「同じ条件を何度も書いて単調だな」と思いました。
ただ、Goの設計思想が少しわかってきたとき、この記述の重要性に気づかされました。
Goでは、エラーは例外ではなく、ただの「値」 です。
なので、処理のの戻り値としてエラーを受け取り、それをを使ってifで分岐をする。
例外を投げて、遠く離れた場所でcatchする、という流れがそもそもありません。
その結果、ファイルを上から下に読むだけで、どこで何がエラーになりそうかわかるようになっています。
- 明示を強制すげ〜となりました。
おわりに
本当はDB操作系やGoルーチン、contextなど、色々なことにも触れたいのですが、言語化できる段階まで理解できていないので、今回はここまでにします。
私自身のレベルが上がったら、自分のために続きの記事を作りたいと思います。
Goでつまずいたおかげで、Laravelが裏で何をやってくれていたのかがより詳細に見えてきました。
まだ全然スラスラとは書けません。
それでも、両方の景色が見えるようになった だけで、片方しか知らなかった頃より、少しマシなコードが書けている気がします。
これからGoを学ぶ方は、ぜひ文法の前に、「設計思想」に目を向けてみてください。
あと、プログラミングしてる感あってGo楽しー!ってなってます。