はじめに
朝日新聞社 Advent Calendar 2021の第20日目を担当するM研の松原です。
宜しくお願いします。
普段はTypeScriptやPythonを触っており、Goを書き始めた当初は「あれもない」「これもない」という物足りなさに満ちていました。
3ヶ月くらいGoと向き合って考え方が変わってきたので、軽量プログラミング言語と比較したGoの特徴や、見てきたドキュメントについてまとめたいと思います。
想定読者
- 一通りのチュートリアルは終えた人
- クラスベースのオブジェクト指向言語に触れたことがある人
- Go言語なんかしっくりこないな〜って人
「Goに入ってはGoに従え」
- Goにないものは結構あるので、まずはそれをちゃんと理解する
- 物事にはトレードオフがつきもの、Goはシンプルさと変更の少なさに重きをおいている
- 「これがないのはイケてない!プンプン」と思うものもあるかもしれないが、時には心を殺すこと(Gopherちゃんを眺めて癒されよう)
- Goの文化ではどうするのが一般的なのかを調べ、イディオムを覚える
軽量プログラミング言語と比較したGo言語の特徴
静的型付け言語、コンパイラによるエラーチェック
型推論ももちろんできる。
x := 4 // ⇒ x はint型
関数型プラグラミングをサポートしない(map、filter、reduceなどがない)
基本的にループで回す。
for i, v := range someList {
someList[i] = v * 2
}
Immutableプログラミングをサポートしない
JS/TSのconstに慣れてると辛みを感じるが、Goでは変数の書き換えは割と一般的。
メモリを節約している。
ゼロ値: 初期化時に代入される値が決まっている
nilを入れられるのはポインタ型のみ。
intの初期値は0なので、それが意味を持ってしまうケースなどは特に注意。(ageが0とか)
ジェネリクスはない
が、近々入るかもしれない。(v1.17では見送られた)
各型に対応した関数を用意するのが慣習。
オーバーロードもないので、関数名の後に型名を付けたりする。
ポインタがある
値渡し(not ポインタ)、参照渡し(ポインタ)がある。
・ tp := &T{}
のように、&でポインタ化
・ p := *tp
のように、*でデリファレンス(ポインタのメモリ番号を元に実体への参照に戻す)
・ Tのポインタ型として *T
で表す
構造体のポインタの場合、デリファレンスせずともポインタから構造体のフィールドやメソッドに直接アクセス出来るため、使うときはあまり意識しない。(引数や返り値などでは意識する)
基本的にポインタを使うときの考え方として、
・ 変更を加えたいとき
・ 巨大な構造体など、値渡し(コピー)のコストが大きいと考えられるとき
にポインタを使う。迷ったらポインタにする。
sliceやmapなど、内部にポインタを持つようなデータ型の場合、冗長なので基本的には使わない。
[]*T
はやるけど *[]*T
はあまりやらないということ。
classではないオブジェクト指向
- struct: データの構造
- interface: ふるまいの制約(メソッド名、Input/Output)
継承はない。structの埋め込みができるが、継承ではなく移譲(delegete)
ダックタイピング的な型付け
・ interfaceのメソッドを全て実装すればそのinterface型としてふるまえる
・ interfaceのメソッドの実装にimplementsのようなキーワードがない
・ コンパイラがインターフェースが使われる箇所でインターフェースを満たしているかを静的にチェックする(なので厳密にはダックタイピングではなくstructual typing)
レシーバを指定した関数を定義することでメソッドを表現する。
レシーバ変数はpythonにおけるメソッドの第一引数のselfに近いイメージ。
固定長の配列 ↔ 可変長のスライス
- 配列:
[5]int{1, 2, 3, 4, 5}
- スライス:
[]int{1, 2, 3, 4, 5}
スライスの実装は構造体になっていて、内部にデータの実体を持つ配列へのポインタを持っている。
パフォーマンス上の特別な理由がない限り、基本的にスライスを使う
nilと空のスライスは意味が異なるので注意。
スライスは長さ(len)とは別に容量(cap)という概念があり、capを超えてappendが走ると実体の配列が新しくメモリに割り当てられ、パフォーマンス的によろしくない。
スライスからスライスへの移し替えなど、事前に個数が決まっている場合はmakeで初期化すること。
関数が多値を返せる
返り値の1つ目がdata, 2つ目をerrとするようなイディオムがある。
Goには例外がなく、エラーを関数の返り値で戻す文化。
例外がない
例外の代わりにpanicがある。
が、復帰が絶望的な時以外では基本的に使わず、多値返却されたerrを if err != nil {}
で処理する。
GCがある
スタックやヒープといったメモリ割当てがあるが、基本的にGoではコンパイラに任せられるためあまり意識しなくて良いことになっている。
ポインタ関連は基本的にヒープに入る。
public/privateなどのアクセス修飾子はない
type名、フィールド名などを大文字始まりにすることでパッケージ外からの参照を可能にする。
小文字始まりにすると同一パッケージ内からしか参照できない。
コンストラクタの特別な構文はない
NewXXXなど、ファクトリ関数を用意するのが慣例。
structのtype名を小文字始まりにして、ファクトリメソッド名を大文字始まりにすればprivateコンストラクタ風なファクトリ関数が実現できる。
コマンド類
モジュールの作成
$ go mod init some_package_name
パッケージのインストール
$ go get -u github.com/hoge_package@some_version
依存関係の整理
$ go mod tidy
CLIツール等のインストール
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
参考資料
A Tour of Go
まずはここから。鉄板のチュートリアル。
Go のモジュール管理【バージョン 1.17 改訂版】
Goにおけるモジュール管理の話。
最近Goを始めた場合、デフォルトでモジュール対応モードになっているはず。
Goを学ぶときにつまずきやすいポイントFAQ | フューチャー技術ブログ
すでに学んでいる知識がかえって学習の妨げになりがちです。このバイアスを除外して(客観化して)、あらためて学ぶというのはなかなか難易度の高いことです。
Effective Go - プログラミング言語 Go ドキュメント v0.1 documentation
Go公式のコーディング規約とかベストプラクティス的なやつ。
golang/go CodeReviewComments 日本語翻訳
ベストプラクティス集的なやつその2。
Privateリポジトリならそこまで気にしなくてもいいルールもある。
The Zen of Go
Goの禅。(ロング版)
pythonと違ってimport thisはできない。
GoのSliceを関数の引数に渡した時の挙動 - Carpe Diem
sliceの話。
関数に渡したsliceを関数内でappendすると予期せぬ出来事が起こるかもしれないので注意。
Goにおけるポインタの使いどころ
ポインタの話。
副作用のある関数を書くケースは比較的少ないと思うが、
構造体は割と大きくなりがちなので、割と気軽に使っていい気がする。
Pointers vs. values in parameters and return values
これもポインタの話。
sliceとかmapみたいなポインタ風の型はポインタにしなくていいみたいな言及がある。
【Go】import 書き方まとめ - Qiita
import文の話。
importの単位はファイルではなくパッケージであることに注意。
基本的にはパッケージ名.XXXでアクセスするのでパッケージ名は分かりやすいものにする。
イケてないのに人気がある golang vs イケてるのに人気がない Nim - 強まっていこう
Go言語disの記事もあるので一応見ておく。
Awesome Go
GitHub - avelino/awesome-go: A curated list of awesome Go frameworks, libraries and software
終わりに
Goは登場時期が遅い割に、イケイケな言語といった印象を感じさせません。
自分もGoを書き始めた当初はシンプルすぎて色々と物足りなさを感じました。
ですが色々と調べていくうちにシンプルさと実用性のバランスをとっている言語だと感じるようになりました。
足りないものを嘆くよりも、早いところ文化を受け入れて染まってしまいましょう。
ジェネリクスはそろそろ入るといいなぁ。
朝日新聞社では、技術職の中途採用を強化しています。
ご興味のある方は下記リンクから希望職種の募集ページに進んでください。
皆様からのご応募、お待ちしております!