Go6 Advent Calendar 2019 の 12/6 の記事になります。
はじめに
新参Golangerのuechocoです。
もともとはブラウザゲームのWebエンジニアでしたが、アプリ化の波に乗って直近では4年半ほどスマートフォンゲームの開発でc++を書いていました。c++のスキルがめっちゃ高くなったかというと初中級程度だと思います。そう言っておかないとその道の猛者たちに問い詰められそうなのでそう言っておきます。そして3ヶ月ほど前にまたWebエンジニアに戻りました。
メインループの中でポインタの寿命や所有権を気にしながら(いうほど気にしてなかったかもしれないけど)常に処理を行い続けるゲーム開発から、(だいぶシンプルに言うと)リクエストを受け取ってレスポンスを返すのWeb開発にパラダイムシフトしました。もちろんWebにはWebの厄介事があり、ネットワークがどうとか、データストレージ(RDBMSなど)がどうとか、非同期メッセージがどうとか、まぁ大変ですね。
何が言いたいかといえば、楽しくやっております(近況報告)。
さて、c++の世界からGoの世界に来て3ヶ月ほど立ちましたので、比較して気づいたことをつらつらと書き留めておこうと思いました。
すんなり馴染めたこと
静的型付け
安心感あるね。
sliceのlenとcapの概念
これはc++のstd::vectorなどのコンテナでも同様の概念があります。
事前にcapを確保することがよいという考え方も、std::vectorなどのreserve()にあたるもので、当然のことと思えました。
便利だと思ったこと
関数ポインタ型に比べて、func型は書きやすい
Go's Declaration Syntax にもそこら辺のことが書いてありますね。もっとも、c++11であればstd::functionなどもあるので、その差はだいぶ縮まったかもしれません。
ガーベジコレクション
よほど変な使い方しなければメモリ勝手に開放してくれるっていいですね。
c++のゲーム開発の中盤から終盤にかけて、メモリの開放漏れを潰していく作業はどこの現場でもありますよね(悲しい目
インターフェースによるダックタイピング
これは書き方の違いかな。c++にもテンプレートでダックタイピングできますけど、テンプレートっていろいろ大変。個人的には、c++のテンプレートとは、開発チームのテンプレートに対する熟練度に合わせてテンプレートも使っていくのがいいと思っています。私自身も使いこなせているわけではないし。と思ってしまうくらいには複雑なものという印象。
一方でGo言語のインターフェースは、個人的な感想ですけどすんなり書けますね。まぁインターフェースを使わないとできないことにすぐにぶち当たるといったほうがいいのかもしれませんが。
気になってしまったこと
string型の引数に怯えてた
func hoge(text string) error {
// 処理
}
なんてことはない文字列を受け取ってなにかの処理をする関数ですが、最初はこれが怖かったんです。 このstringってメモリ全コピーされないの?これはCopy-On-Writeとか最適化かかっている?ポインタにしないで使っているのやばくない?どのくらいの文字列長なら気にしないでいいとかある? とか考えてました。c++では文字列型にstd::string
クラスを用いることが多いと思いますが、値を変更する必要のない文字列を引数に与えるときはたいていconst std::string&
のように明示的に参照渡しかつ変更しないことを指定していたのです。
どうやらGoのstringは、文字列データに対する長さとスライスを格納するstructのようなもので、string型をコピーしただけでは、長さとスライスのポインタアドレスがコピーされるだけのようでした。メモリ全コピーのような高コストなことはなさそうでした。
- Arrays, slices (and strings): The mechanics of 'append' - The Go Blog
- Does Go language use Copy-on-write for strings - Stack Overflow
ポインタ気軽に使えすぎてぬるぽへの恐怖が薄れてきた
ちょっと郷に従いすぎてしまったんでしょうか。
- ドット演算子が有能すぎてポインタであるかどうかを意識しない。
- あまりにも気軽にポインタ型を作れて返却して引き回してしまう。
- ポインタの所有権や寿命に関して意識することがない。
- レシーバーも大抵はポインタで書いてしまうことが多いし。
- err != nil はお決まりのフレーズ。
- 総じて、ポインタというものへの取り扱いが雑になってしまった。
その結果 *data.hoge
って書いたときにたまにnilぽしてpanicする。
いや、熟練度が足りていないだけです。
ただ、c++時代に比べると、ポインタに対する意識がほんとに変わってしまいました。
ガッ
ranged-for的に書こうとしてポインタでやらかした
実際に業務でやらかした事例です。
structのコピーに抵抗があったので、map化するときにポインタを取得しようとしたんですけど、ハマりました。
// https://play.golang.org/p/uWlsye5ovBl
package main
import (
"fmt"
)
type SomeModel struct {
ID uint
Name string
State uint
}
func main() {
models := make([]SomeModel, 0)
m1 := SomeModel{ID: 1, Name: "田中", State: 1}
m2 := SomeModel{ID: 2, Name: "佐藤", State: 2}
m3 := SomeModel{ID: 3, Name: "池田", State: 5}
models = append(models, m1)
models = append(models, m1)
models = append(models, m2)
models = append(models, m3)
if err := save(models); err != nil {
fmt.Printf("%s", err.Error())
panic(0)
}
}
func save(models []SomeModel) error {
countMap := make(map[uint]uint, len(models))
modelMap := make(map[uint]*SomeModel, len(models))
for _, model := range models {
countMap[model.ID]++
if _, ok := modelMap[model.ID]; !ok {
modelMap[model.ID] = &model // ココ
}
}
// TODO: DEBUG CODE 消す
fmt.Printf("countMap: %+v\n", countMap)
for k, v := range modelMap {
fmt.Printf("modelMap[%d] = %v (p=%p)\n", k, *v, v)
}
// ... 処理
return nil
}
save()
メソッドのPrintfの結果、こうなりました。
countMap: map[1:2 2:1 3:1]
modelMap[1] = {3 池田 5} (p=0x40a0f0)
modelMap[2] = {3 池田 5} (p=0x40a0f0)
modelMap[3] = {3 池田 5} (p=0x40a0f0)
期待していた結果はこちらでした(修正後: https://play.golang.org/p/1SAkmkR24hn)
countMap: map[1:2 2:1 3:1]
modelMap[1] = {1 田中 1} (p=0x432100)
modelMap[2] = {2 佐藤 2} (p=0x432120)
modelMap[3] = {3 池田 5} (p=0x432130)
c++11には、ranged-forという構文がありまして、Goのrangeとよく似た書き方でループ処理が書けたりします。
std::vector<Data> v;
for (const Data& elem : v) {
// 処理
}
このranged-for構文は、受け取る変数の型が指定できるのですが、 Data&
const auto&
のように参照渡しで書くことが多いです。Goのfor/rangeは構文がよく似ているので「参照渡しされているポインタをmapに詰め直せば良い」と思い込んでしまいました。実際には参照渡しされていなかったというわけです。先入観は良くないですね、、、
標準関数の少なさ。algorithm.hがない
これはc++と比較しなくてもよく言われていることだと思います。
c++にはalgorithm.hというstdコンテナに対する便利ライブラリがあります。sortはGoにもありますが、unique, find_if, remove_ifとかがありません。同等のコードを一体何回書いただろうか、、、
おわりに
Goの正規表現なんとかならないの。