PHPエンジニアが Go をいきなり業務で使った感想

エンジニア(業務委託) の @junkitamura です。サーバーサイドエンジニアをしています。

この記事は [Photocreate Advent Calendar 2018](https://qiita.com/advent-calendar/2018/photocreate) の 18日目 の記事です。

## はじめに

　簡単な自己紹介は  [Photocreate Advent Calendar 2018](https://qiita.com/advent-calendar/2018/photocreate) の [8日目の記事](https://qiita.com/junkitamura/items/9b85f5d86441d2f35fd0)を参照してください。
　`フォトクリエイト`さんでは、いわゆる`クラウド化`に向けて各種プロジェクトが進行中で、その中に [AWS Batch](https://aws.amazon.com/jp/batch/) を使う案件があります。このサービスでは `Docker イメージ`でジョブ定義を作成し、ジョブキューを送信する事で、指定のコンピューティング環境上でバッチ処理を実行させる事ができます。
　この Docker イメージ内で実行されるプログラム(アプリケーション)の言語として`Go言語`が選ばれました。ぱちぱちぃ:clap:

　とあるプロジェクトを終えて、「さて次は何を…Laravel かな…？」というタイミングでクラウド化のプロジェクトにアサインされました。
　`PHP` エンジニアとして`フォトクリエイト`さんで初めて `Symfony`(2.8) に触れてから約1年、ようやくフレームワークに慣れてきたところで、いきなり`Go言語`へ。


　`AWS` も触ったことがなく色々と初めてづくしでしたが`Go言語`を使ってみて`戸惑った事`や`感心した事`といった感想をまとめます。

## Go言語の感想

　元々 `C` や `Java` 等も(必要なシーンで)扱っておりましたので、「PHPエンジニアが…」という感想では無いですがご了承ください。

### オブジェクト指向から離れましょう

　type .. struct ? レシーバー…関数名……あぁ `class`ね！(:no_good:)
　PHP や Java で`オブジェクト指向`プログラミングをしてきた人が`陥りやすい罠`です。特に斜め読みしていると落ちます。
　`構造体`って`プロパティの集まり`でしょ？`メソッド`(レシーバーと関数)、`インターフェース` ときたら「クラスじゃん？」となって、さらに`埋め込み`(`embedding`)を`継承`と**誤解**しちゃうと抜けるのに苦労することになります。
　`構造体(struct)`は名前(name)と型(type)を持つフィールド(field)というものの集まりと説明されています。[Struct types](https://golang.org/ref/spec#Struct_types)。
　`struct` 構造(体)は `type`によって識別子(型名)に`型`として結びつけられます。
　さらに混乱の元となるのが `new 関数`です。これは `クラスをインスタンス化するものではありません`。 あくまで各フィールドをゼロ化した構造体のメモリ領域を確保するだけです。初期値が与えられたり、コンストラクタが呼びだされたり、そういう事はいっさい発生しません。
　[オブジェクト指向ちっくな書き方 (play.golang.org)](https://play.golang.org/p/Bp8KKxqbN53)。見た目にだまされないで！:imp:

<details><summary>オブジェクト指向ちっくな書き方</summary><div>

```go:オブジェクト指向ちっく
package main

import "fmt"

// 鳴かせるインターフェース
type Animal interface {
	MakeSound() string
}

// Pet 構造体
type Pet struct {
	Color string
	Name  string
}

// Dog 構造体
type Dog struct {
	Pet
}

// Dog の鳴き方
func (dog *Dog) Bow() string {
	return "bow wow!"
}

// インターフェース実装
func (dog *Dog) MakeSound() string {
	return fmt.Sprintf("'%s' barked the %s dog named %s.", dog.Bow(), dog.Color, dog.Name)
}

// Cat 構造体
type Cat struct {
	Pet
}

// Cat の鳴き方
func (cat *Cat) Meow() string {
	return "meow mew..."
}

// インターフェース実装
func (cat *Cat) MakeSound() string {
	return fmt.Sprintf("'%s' purred the %s cat named %s.", cat.Meow(), cat.Color, cat.Name)
}

// Animal なら 鳴ける
func makeSound(pet Animal) {

	fmt.Println(pet.MakeSound())

}

// 鳴かせてみる
func main() {

	// 犬
	dog := new(Dog)
	dog.Color = "black"
	dog.Name = "Jiro"

	// 猫
	cat := new(Cat)
	cat.Color = "white"
	cat.Name = "Tama"

	// まずは犬
	makeSound(dog)

	// そして猫
	makeSound(cat)

}
```
</div></details>

実行結果

```text
'bow wow!' barked the black dog named Jiro.
'meow mew...' purred the white cat named Tama.
```

### きっちり型にハメましょう

　`PHPの良さって何ですか？`と聞かれた時にメリットやデメリットがいろいろと思い浮かびますが、どちらなのか悩むのが`ゆるく扱われる型`です。さらには`可変変数`という仕様。PHPでは`動的型付け`が行なわれるため、整数として宣言した変数を文字列として扱う事ができます。例えば`1`という数値を`0x01`ではなく`1`という文字として扱ってくれます。初めてPHPに触れたときは衝撃的:flushed:でした。`可変変数`に至っては`なんじゃこりゃ...`:scream_cat:と驚愕したものです。

<details><summary>PHP のゆるい型と可変変数</summary><div>

```php
<?php

$strA = 'abc';
$intB = intval(1);

// 整数と文字列の結合
echo $strA . $intB;
// 結果: abc1

// abc1 という変数に文字列を代入
$abc1 = "var abc1";
// (strA という変数 と intB という変数の結合) という(名前の)変数を表示
echo ${($strA . $intB)};
// 結果: var abc1

// new にも使えます
$strC = 'xyz';

class xyz1abc {
    public $foo = "C-B-A";
}
$className = $strC . $intB . $strA;
echo (new $className)->foo;
// 結果: C-B-A

class abcxyz1 {
    public $foo = "A-C-B";
}
$className = $strA . $strC . $intB;
echo (new $className)->foo;
// 結果: A-C-B
```

</div></details>


　`型を明示的に宣言しなくても良い`というのは`Go言語`では`代入演算子(:=)`が用意されています。右辺がリテラルならばその型、関数ならば関数の戻り型となります。この時点で型が確定となるため、PHPのように`文字列`と`数値`を演算子を使って結合することはできません。`数値を文字列に変換してから結合`となります。

```php:PHP
$a = 1;
$b = 'abc';
$c = $b . $a; // abc1
```

```go:Go
a := 1
b := "abc"
c := fmt.Sprintf("%s%d", b, a) // abc1
もしくは
c := b + strconv.Itoa(a) // abc1
もしくは...(略)
```

### 配列は固定長

　PHPでは`配列の長さなんてものを気にしたことがない！`という方が多いのではないかと思います。変数名の後ろに`[]`を付けて代入するだけで`配列が延びる`のですから便利この上ないです。

```PHP:PHPの配列
$arr = [1, 2, 3];
echo count($arr); // 3

$arr[] = 4;
$arr[] = 5;
(..snip..)
$arr[] = 10;
echo count($arr); // 10
```

　Go言語では配列は固定長のみです。配列の要素を箱:package:と例えるのであれば、最初から箱の数が解っていないと使えません。例えば`月(month)`を格納するには1月から12月まで12個の箱を用意すれば良い事になります。
　一方で`日(day)`は 28の時もあれば30、もしくは31の時もあります。最大で31なので31個用意しておいて使わない箱には何も入れない、という使い方が良さそうです。
　友達参加:two_women_holding_hands:も自由だよ〜:ok_woman:という忘年会:beers:では参加する人数などは事前にわかりません。こういう場合、Go言語では`スライス(slice)`を使います。最初に3人来る事は確定しており、その後は何人くらいくるか解りません。現時点では最大で5人くらいかなぁ…という時には、`make([]string, 3, 5)` と宣言し、スライス型の変数を作成します。最初の`3`は`長さ(length)`を指定し次の`5`は`キャパシティ(capacity)`を指定します。
　3人目まで(`a[0]`, `a[1]`, `a[2]` まで)は箱が用意されているので、添え字付き変数にそのまま代入できます。
　4人目以降は箱がありません(長さ3なので)。そこで参加者が増えるたびに箱を増やしていかなくてはなりません。PHPでは何も考えずに箱を増やせていたのですが(実はそういうわけでは無いけど)、Go言語では`メモリ確保(memory allocation)`という事を少しだけ考えなくてはなりません。今回は`キャパシティ(capacity)`(意訳すると`許容量`)を`5`に指定しています。つまり、箱5つまでは(メモリを)`確保済み`だよ、という事になります。`append()`関数でスライスに要素を追加していくと、5つ目まではキャパシティの大きさは変わりません。6つ目を追加すると(今回の場合は)`10`になります。この時、`10箱分(のメモリ)が新たに確保されている`状態になります。言い換えると、メモリ空間の別の場所に10箱分のスペースを用意した状態になります。スライスを代入している変数はポインタなので、先頭の箱のアドレスを示します。メモリが新たに別の場所に確保されたため、先頭の箱の場所も変わっており、つまりアドレスも変わっています。
　気をつけなくてはならないのは、`新たにメモリ空間を確保する`という処理が`意外と重い`:tired_face:ということ。PHP や Java で `大量の new はパフォーマンスを悪くする`といわれているのは`インスタンス化によるメモリ確保`(と初期化処理)のコストが高いからです。
　`メモリ確保を繰り返す処理は避けた方が良い`のは Go言語 でも同じです。
　「ならばキャパシティを最初から1万とか大きくしておけば良いじゃん。」と思うかもしれませんが、限られたメモリ空間を使いもしないのに大きく陣取るのは..NGです:no_good:
　バランスを考えてスライスの`長さ(length)`と`キャパシティ(capacity)`を設定しなくてはなりません。PHPでは考えなくても良い話でしたので、混乱するかもしれません。(ほんとはPHPでもメモリを考えた方が良いんだけどね:laughing:)
　[array と slice (play.golang.org)](https://play.golang.org/p/vrRDX0j5gL3)

<details><summary>array と slice</summary><div>

```go:array,slice
package main

import (
	"fmt"
)

func main() {
	// array
	a := [3]string{"aaa", "bbb", "ccc"}
	fmt.Println(a) // [aaa bbb ccc]

	// slice
	s := make([]string, 3, 5)
	fmt.Printf("sp:%p\n", s)	// sp:0xc4200900f0
	s[0] = "aaa"
	s[1] = "bbb"
	s[2] = "ccc"
	// s[3] = "ddd" ← エラー[panic: runtime error: index out of range]
	fmt.Printf("len:%d cap:%d sp:%p\n", len(s), cap(s), s)	// len:3 cap:5 sp:0xc4200900f0
	s = append(s, "ddd")	// s[3]
	fmt.Printf("len:%d cap:%d sp:%p\n", len(s), cap(s), s)	// len:4 cap:5 sp:0xc4200900f0
	s = append(s, "eee")	// s[4]
	// --- ここで capacity いっぱい.
	fmt.Printf("len:%d cap:%d sp:%p\n", len(s), cap(s), s)	// len:5 cap:5 sp:0xc4200900f0
	// --- さらに要素を追加する
	s = append(s, "fff")	// s[5]
	fmt.Printf("len:%d cap:%d sp:%p\n", len(s), cap(s), s) // len:6 cap:10 sp:0xc4200a0000
	// --- capacity 延び、さらにポインタのアドレスも変わった.

	s = append(s, "ggg")	// s[6]
	s = append(s, "hhh")	// s[7]
	s = append(s, "iii")	// s[8]
	fmt.Printf("len:%d cap:%d sp:%p\n", len(s), cap(s), s) // len:9 cap:10 sp:0xc4200a0000
	s = append(s, "jjj")	// s[9]
	// --- ここで延びた capacity もいっぱいに.
	fmt.Printf("len:%d cap:%d sp:%p\n", len(s), cap(s), s) // len:10 cap:10 sp:0xc4200a0000
	// --- さらに要素を追加する
	s = append(s, "kkk")	// s[10]
	fmt.Printf("len:%d cap:%d sp:%p\n", len(s), cap(s), s) // len:11 cap:20 sp:0xc420090000
	// --- capacity がさらに延び、ポインタのアドレスも変わった.

	fmt.Println(s) // [aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk]

}
```

</div></details>

### 制御系とか

　特に困ることはありませんが、制御系に多少の違いがあります。

- do 〜 while が無い
- for 〜 each も無い(似た構造はある)
- switch に break が要らない
- goto がある:bangbang::eyes:

　最後の `goto` が驚きでした。`N88−BASIC`の時代から`構造化プログラミング`を叫ぶ方々には`悪魔の子ダミアン`:smiling_imp:のように扱われていた`goto`が~~ここにきて復活~~ _(コメントいただき修正しました)_、新たに設計された言語にもかかわらず残されているという…。当時は`goto 120`のように`行番号`を指定していましたが Go言語ではラベルを指定します。
　使いどころが解らないのですが、下記のように二重ループを抜ける時に使うのかなぁ…と。ちなみに`goto FinLoop`を`break`にすると`j ループは抜けるけど i ループは続きます`。_直近のループからのみ脱出(break)_します。((i,j) が (0,2) の次に (1,0) (1,1) (1,2) (2,0) ...と続き (9,2) で終わる)

```go:goto,label
for i := 0; i < 10; i++ {
	for j := 0; j < 10; j++ {
		if j == 3 {
			goto FinLoop
		}
		fmt.Printf("i:%d, j:%d\n", i, j)
	}
}
FinLoop:
	fmt.Println("exit loop")
```

実行結果

```text
i:0, j:0
i:0, j:1
i:0, j:2
exit loop
```

### 意外と気づかずにやってしまうこと:warning: その他の感想:thought_balloon:

#### § 小文字で始まるものはすべからく`private`になる

- 小文字で始まる`変数`は`private`になる
- 小文字で始まる`関数`も`private`になる
- 小文字で始まる`構造体のメンバ`も`private`になる

　ついつい、テスト用にパッケージ名を`foobar_test`とした時に、小文字で始まるメンバにアクセスできなかったりして:disappointed:悩んじゃったりする...人もいるかもしれない。てへぺろ:yum:

#### § 変数名がパッケージ名と同じだった

　`GoLand`のような統合開発環境を使っていれば問題ないのでしょうが、変数でエラーになる時は`変数名`が`パッケージ名`と同じ名前にしてしまっていないか確認してみましょう。(`import "github.com/aws/aws-sdk-go/aws/session"` としているのに `session := "セッション"` と同名の変数を使うとエラーとなります)

#### § リンカとかプリプロセッサは無いの？

　`C言語`などのコンパイラはプリプロセッサによるマクロ処理などがありますが Go言語にはありません。リンカは`go`コマンドに含まれています。
　プリプロセッサが無いので`#ifdef`等のように条件をつけてビルドができない、つまり`windows の時のみ`などの指定ができない、とお考えであれば心配ご無用です:ok_hand:。`ビルドタグ`というものがありビルド時に条件を指定できます。
　[Build Constraints](https://golang.org/pkg/go/build/#hdr-Build_Constraints)

#### § バイナリが...ぽっちゃり系

　`go build` でビルドした後の実行ファイル(バイナリ)が~~デブ~~ぽっちゃり系です。`コンパイルされたバイナリ`という言葉のイメージとのギャップに驚きます:fearful:。
　`ldflags`オプションでリンカへオプションが渡せます。Linux系OSで使う場合は、多少のダイエット:sweat_drops:ができます。他には`UPX`を使う方法などもあるようです。詳しくは [The Go binary diet](https://medium.com/techmantra/the-go-binary-diet-e1af08bcdfa2) を参照してください。

```bash
$ go build -ldflags="-w"
```

- `-w` DWARF(Linux系の`ELF`のデバッグ情報)を生成しなくなります
- `-s` デバッグ用のシンボルテーブルを生成しない(...らしいのですが、環境依存なのか、このオプションを指定してもバイナリファイルは小さくなりませんでした。指定の仕方が悪いのかも)

#### § 並行処理を欲張らない

　Go言語では比較的簡単に並行処理を書くことができます。`goroutine`と呼ばれており、`go func() {...}`と書くだけで関数を平行処理してくれます。

- 指定された AWS Bucket 内の全オブジェクトのサイズを取得する
- ローカルディレクトリ内のファイルを全て AWS S3 へアップロードする
- 自社サービスの API を同時にコールする

　...など、並行処理を行なうと(全体的な)処理が早く終わるケースが多々あります。
　あまり考えずにループ:arrows_clockwise:を組んで平行処理してしまうと、

- 接続先のアクセス数制限にひっかかる
- 回線がパンクする
- ファイルを開きすぎて OS から怒られる(`too many open files`とか)
- エラーが発生しているのに次から次へとループが進んでしまう(そして全てエラーになる)

　...などなど弊害が発生します。
　強力なモーターを積んで軽量化したミニ四駆がコーナーを勢いよく飛び出してしまった失敗経験:trollface:が全く役にたっていません。

　対策例

- `チャンネル(channel)`や[`セマフォ(semaphore)`](https://godoc.org/golang.org/x/sync/semaphore)で同時実行数を制限する:construction:
- [`エラーグループ(errgroup)`](https://godoc.org/golang.org/x/sync/errgroup)と[`コンテキスト(context)`](https://golang.org/pkg/context/) でエラーが発生したら全部止まるようにしておく:no_entry:
- [`Mutex`](https://golang.org/pkg/sync/)で排他制御したりする:lock:
    - スライスに結果を溜め込みたいだけなら`チャンネル(channel)`にロック機能ついてるから`mutex`などで無駄なロックをかけない:closed_lock_with_key:
- (特にコネクト失敗などの)エラー時は[`backoff`](https://github.com/cenkalti/backoff)等で間隔をあけてリトライ:repeat:する

　実行環境のスペックや制限(OSのファイルオープン可能数、使えるメモリ容量、ディスクスペース、利用可能な i-node 数、通信回線の太さ、接続先の接続可能数...その他いろいろ)などを考慮しつつリミッター:underage:を設定しバランスの良い平行数:100:で処理するようにしましょう。

## まとめ

- Go言語の`特性を生かせるシーン`で使うべき
- C言語で fork してました、とか Java でスレッドやってました、と並列もしくは平行処理に慣れていないと、`goroutine は最初はきつい`かも
- `ライブラリが充実`していて楽チン

<!--
## 明日は

明日の記事は @%{@アカウント} さんが %{テーマ／タイトル} について です。
（次の人が更新したらあとで記事を更新してリンクを自分で張る）

-->
---

フォトクリエイトでは`Go言語バッチリです！`:+1:、`AWS 任せてください〜`:laughing: ...な方からの [ご応募をお待ちしております](https://recruit.photocreate.co.jp/entry/?utm_source=qiita&utm_medium=advc2018&utm_campaign=c2443b7e5d69b4f35938)。