QualiArts Advent Calendar 2019、7日目担当の朝倉です。
スマートフォンゲームのバックエンドにGoを使いはじめて半年たったのでその中で感じたことを書きます。
はじめに
QualiArtsのスマートフォンゲームのバックエンドは、これまでJavaやNodeJSで開発していました。
今年の5月ごろから新規タイトルの開発がスタートし、新たなチャレンジとしてGoを採用して開発を進めています。
現在、新規タイトルのバックエンドエンジニアは私を含め5名で、その全員がもともとJavaエンジニアです。
長年Javaエンジニアだった私達がGoをやりはじめて感じたことを書きたいと思います。
(JavaがいいとかGoがいいとか言語の優劣を話したい訳ではありません)
JavaからGoをやりはじめて感じたこと
静的型付け、コンパイル言語の安心感
スマートフォンゲームは、リリース後の運用での開発も活発で場合によっては何年も運用するので静的型付けでコンパイル時にチェックできるのはJavaと同じく安心感を感じます。
静的型付けのおかげでIDEでのコード補完も効きやすいし、他のメンバーが書いたコードも追いやすいです。
コードの統一がしやすい
Goは標準でformatterやlinterががついているのでコードの統一がしやすいです。
標準でついてるのでコードの書き方などでチームメンバー内で対立することも少ないです。
私達のチームでは複数のlinterを組み合わせていて、不要なコードや冗長な書き方も指摘してくれるのでコードを綺麗に保つことができてるかなと思っています。
標準機能でだいたいのことができる
formatterやlinterの他にも、テストやテンプレートエンジンなどもGoの標準機能に含まれています。
私達のチームでは標準のテンプレートエンジンを使って各種コードや定義ファイルなどの自動生成を行っています。
また、ベンチマークやプロファイルの機能も標準でが簡単に取れるので、Javaで開発していたときよりも頻繁に計測している気がします。
コンパイル、起動が早い
Javaと比較してコンパイルやプログラムの起動が早いです。Javaで開発していたタイトルと比べるとまだコードの量が少ないですが、コード書いてコンパイルして動作確認のストレスが少ないです。
また、Javaの時はSpringBootを使っていたのもあって起動にかなり時間がかかっていましたが、Goは起動も早いのでオートスケールとの相性も良さそうです。
Interfaceに慣れるの時間かかる
GoのInterfaceはJavaのように明示的にimplementsする形ではなく、interfaceの中にある関数と同じシグニチャの関数が全て実装されているとそのinterfaceの型として扱うことができます。
明示的なinterfaceに慣れていたので、Goのinterfaceの扱い方に慣れるのに苦労しました。
type Animal interface {
Cry() string
}
type Dog struct {
}
func (d Dog) Cry() string {
return "ワン"
}
func AnimalCry(animal Animal) {
fmt.Println(animal.Cry())
}
func main() {
// DogはAnimalインターフェースを満たしているのでAnimal型の引数に渡すことができる
AnimalCry(Dog{})
}
ちなみにGoにはJavaのような継承がないですが、interfaceで満たせることが多いので今のところ継承がなくて困ることは少ないです。
Collection操作が大変
JavaにあるGenericやStreamAPIなどはないので、GoでSlice(JavaのListのようなもの)やMapの操作は、下記のように結構ゴリゴリの実装が必要で大変です。
// フィルター関数
func Filter(scores []int) []int {
ret := make([]int,0)
for _,v := range scores {
if v > 50 {
ret = append(ret, v)
}
}
return ret
}
// Map変換
func ToMap(cards []*Card) map[int]*Card {
ret := make(map[int]*Card)
for _, card := range cards {
ret[card.ID] = card
}
return ret
}
ゲームロジックで頻繁にSliceやMapの操作することがあるので、マスタデータやユーザデータを表すSliceの操作は自動生成しようかと考えています。
例外処理が大変
GoにはJavaのようなtry〜catch〜finallyやthrowのようなエラー処理はありません。
戻り値としてエラーを表すerrorインターフェースを返すことでエラー処理をします。
呼び出し元では、戻り値で返ってくるエラーを処理(さらに呼び出し元にエラーを戻すなど)する必要があります。
func Func1(ctx context.Context) error {
err := Func2(ctx)
// error処理
if err != nil {
return err
}
・・・
}
Goで開発を始めた当初は面倒だなと思ってましたが、半年したらこのエラー処理にも慣れました。
エラー処理がされているかもlinterでチェックしているのでエラー処理を忘れることもなさそうです。
3項演算子が使いたくなる
Goには3項演算子がないので、ちょっとした分岐でも下記のようにifで分岐が必要です。
func Func(isSuccess bool) int {
value := 0
if isSuccess {
value = 10
}
return value
}
特に困ることはないのですが、3項演算子使いたいなぁとたまに思ったりします。
おわりに
今回は、Javaエンジニアだった私達がGoで開発をはじめて感じたことを紹介してみました。
まだGoを使い始めたばかりで日々模索しながら開発を進めています。
これからもGoの開発で経験したことを発信できればと思います。