13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Go6Advent Calendar 2019

Day 6

c++ゲームエンジニアからGolang Webエンジニアになりましてはや数ヶ月。

Last updated at Posted at 2019-12-05

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型をコピーしただけでは、長さとスライスのポインタアドレスがコピーされるだけのようでした。メモリ全コピーのような高コストなことはなさそうでした。

ポインタ気軽に使えすぎてぬるぽへの恐怖が薄れてきた

ちょっと郷に従いすぎてしまったんでしょうか。

  • ドット演算子が有能すぎてポインタであるかどうかを意識しない。
  • あまりにも気軽にポインタ型を作れて返却して引き回してしまう。
  • ポインタの所有権や寿命に関して意識することがない。
  • レシーバーも大抵はポインタで書いてしまうことが多いし。
  • 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の正規表現なんとかならないの。

13
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?