エンジニア(業務委託) の @junkitamura です。サーバーサイドエンジニアをしています。
この記事は Photocreate Advent Calendar 2018 の 18日目 の記事です。
はじめに
簡単な自己紹介は Photocreate Advent Calendar 2018 の 8日目の記事を参照してください。
フォトクリエイトさんでは、いわゆるクラウド化に向けて各種プロジェクトが進行中で、その中に AWS Batch を使う案件があります。このサービスでは Docker イメージでジョブ定義を作成し、ジョブキューを送信する事で、指定のコンピューティング環境上でバッチ処理を実行させる事ができます。
この Docker イメージ内で実行されるプログラム(アプリケーション)の言語としてGo言語が選ばれました。ぱちぱちぃ![]()
とあるプロジェクトを終えて、「さて次は何を…Laravel かな…?」というタイミングでクラウド化のプロジェクトにアサインされました。
PHP エンジニアとしてフォトクリエイトさんで初めて Symfony(2.8) に触れてから約1年、ようやくフレームワークに慣れてきたところで、いきなりGo言語へ。
AWS も触ったことがなく色々と初めてづくしでしたがGo言語を使ってみて戸惑った事や感心した事といった感想をまとめます。
Go言語の感想
元々 C や Java 等も(必要なシーンで)扱っておりましたので、「PHPエンジニアが…」という感想では無いですがご了承ください。
オブジェクト指向から離れましょう
type .. struct ? レシーバー…関数名……あぁ classね!(
)
PHP や Java でオブジェクト指向プログラミングをしてきた人が陥りやすい罠です。特に斜め読みしていると落ちます。
構造体ってプロパティの集まりでしょ?メソッド(レシーバーと関数)、インターフェース ときたら「クラスじゃん?」となって、さらに埋め込み(embedding)を継承と誤解しちゃうと抜けるのに苦労することになります。
構造体(struct)は名前(name)と型(type)を持つフィールド(field)というものの集まりと説明されています。Struct types。
struct 構造(体)は typeによって識別子(型名)に型として結びつけられます。
さらに混乱の元となるのが new 関数です。これは クラスをインスタンス化するものではありません。 あくまで各フィールドをゼロ化した構造体のメモリ領域を確保するだけです。初期値が与えられたり、コンストラクタが呼びだされたり、そういう事はいっさい発生しません。
オブジェクト指向ちっくな書き方 (play.golang.org)。見た目にだまされないで!![]()
オブジェクト指向ちっくな書き方
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)
}
実行結果
'bow wow!' barked the black dog named Jiro.
'meow mew...' purred the white cat named Tama.
きっちり型にハメましょう
PHPの良さって何ですか?と聞かれた時にメリットやデメリットがいろいろと思い浮かびますが、どちらなのか悩むのがゆるく扱われる型です。さらには可変変数という仕様。PHPでは動的型付けが行なわれるため、整数として宣言した変数を文字列として扱う事ができます。例えば1という数値を0x01ではなく1という文字として扱ってくれます。初めてPHPに触れたときは衝撃的
でした。可変変数に至ってはなんじゃこりゃ...
と驚愕したものです。
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
型を明示的に宣言しなくても良いというのはGo言語では代入演算子(:=)が用意されています。右辺がリテラルならばその型、関数ならば関数の戻り型となります。この時点で型が確定となるため、PHPのように文字列と数値を演算子を使って結合することはできません。数値を文字列に変換してから結合となります。
$a = 1;
$b = 'abc';
$c = $b . $a; // abc1
a := 1
b := "abc"
c := fmt.Sprintf("%s%d", b, a) // abc1
もしくは
c := b + strconv.Itoa(a) // abc1
もしくは...(略)
配列は固定長
PHPでは配列の長さなんてものを気にしたことがない!という方が多いのではないかと思います。変数名の後ろに[]を付けて代入するだけで配列が延びるのですから便利この上ないです。
$arr = [1, 2, 3];
echo count($arr); // 3
$arr[] = 4;
$arr[] = 5;
(..snip..)
$arr[] = 10;
echo count($arr); // 10
Go言語では配列は固定長のみです。配列の要素を箱
と例えるのであれば、最初から箱の数が解っていないと使えません。例えば月(month)を格納するには1月から12月まで12個の箱を用意すれば良い事になります。
一方で日(day)は 28の時もあれば30、もしくは31の時もあります。最大で31なので31個用意しておいて使わない箱には何も入れない、という使い方が良さそうです。
友達参加
も自由だよ〜
という忘年会
では参加する人数などは事前にわかりません。こういう場合、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箱分のスペースを用意した状態になります。スライスを代入している変数はポインタなので、先頭の箱のアドレスを示します。メモリが新たに別の場所に確保されたため、先頭の箱の場所も変わっており、つまりアドレスも変わっています。
気をつけなくてはならないのは、新たにメモリ空間を確保するという処理が意外と重い
ということ。PHP や Java で 大量の new はパフォーマンスを悪くするといわれているのはインスタンス化によるメモリ確保(と初期化処理)のコストが高いからです。
メモリ確保を繰り返す処理は避けた方が良いのは Go言語 でも同じです。
「ならばキャパシティを最初から1万とか大きくしておけば良いじゃん。」と思うかもしれませんが、限られたメモリ空間を使いもしないのに大きく陣取るのは..NGです![]()
バランスを考えてスライスの長さ(length)とキャパシティ(capacity)を設定しなくてはなりません。PHPでは考えなくても良い話でしたので、混乱するかもしれません。(ほんとはPHPでもメモリを考えた方が良いんだけどね
)
array と slice (play.golang.org)
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]
}
制御系とか
特に困ることはありませんが、制御系に多少の違いがあります。
- do 〜 while が無い
- for 〜 each も無い(似た構造はある)
- switch に break が要らない
- goto がある

最後の goto が驚きでした。N88−BASICの時代から構造化プログラミングを叫ぶ方々には悪魔の子ダミアン
のように扱われていたgotoがここにきて復活 (コメントいただき修正しました)、新たに設計された言語にもかかわらず残されているという…。当時はgoto 120のように行番号を指定していましたが Go言語ではラベルを指定します。
使いどころが解らないのですが、下記のように二重ループを抜ける時に使うのかなぁ…と。ちなみにgoto FinLoopをbreakにするとj ループは抜けるけど i ループは続きます。_直近のループからのみ脱出(break)_します。((i,j) が (0,2) の次に (1,0) (1,1) (1,2) (2,0) ...と続き (9,2) で終わる)
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")
実行結果
i:0, j:0
i:0, j:1
i:0, j:2
exit loop
意外と気づかずにやってしまうこと
その他の感想
§ 小文字で始まるものはすべからくprivateになる
- 小文字で始まる
変数はprivateになる - 小文字で始まる
関数もprivateになる - 小文字で始まる
構造体のメンバもprivateになる
ついつい、テスト用にパッケージ名をfoobar_testとした時に、小文字で始まるメンバにアクセスできなかったりして
悩んじゃったりする...人もいるかもしれない。てへぺろ![]()
§ 変数名がパッケージ名と同じだった
GoLandのような統合開発環境を使っていれば問題ないのでしょうが、変数でエラーになる時は変数名がパッケージ名と同じ名前にしてしまっていないか確認してみましょう。(import "github.com/aws/aws-sdk-go/aws/session" としているのに session := "セッション" と同名の変数を使うとエラーとなります)
§ リンカとかプリプロセッサは無いの?
C言語などのコンパイラはプリプロセッサによるマクロ処理などがありますが Go言語にはありません。リンカはgoコマンドに含まれています。
プリプロセッサが無いので#ifdef等のように条件をつけてビルドができない、つまりwindows の時のみなどの指定ができない、とお考えであれば心配ご無用です
。ビルドタグというものがありビルド時に条件を指定できます。
Build Constraints
§ バイナリが...ぽっちゃり系
go build でビルドした後の実行ファイル(バイナリ)がデブぽっちゃり系です。コンパイルされたバイナリという言葉のイメージとのギャップに驚きます
。
ldflagsオプションでリンカへオプションが渡せます。Linux系OSで使う場合は、多少のダイエット
ができます。他にはUPXを使う方法などもあるようです。詳しくは The Go binary diet を参照してください。
$ go build -ldflags="-w"
-
-wDWARF(Linux系のELFのデバッグ情報)を生成しなくなります -
-sデバッグ用のシンボルテーブルを生成しない(...らしいのですが、環境依存なのか、このオプションを指定してもバイナリファイルは小さくなりませんでした。指定の仕方が悪いのかも)
§ 並行処理を欲張らない
Go言語では比較的簡単に並行処理を書くことができます。goroutineと呼ばれており、go func() {...}と書くだけで関数を平行処理してくれます。
- 指定された AWS Bucket 内の全オブジェクトのサイズを取得する
- ローカルディレクトリ内のファイルを全て AWS S3 へアップロードする
- 自社サービスの API を同時にコールする
...など、並行処理を行なうと(全体的な)処理が早く終わるケースが多々あります。
あまり考えずにループ
を組んで平行処理してしまうと、
- 接続先のアクセス数制限にひっかかる
- 回線がパンクする
- ファイルを開きすぎて OS から怒られる(
too many open filesとか) - エラーが発生しているのに次から次へとループが進んでしまう(そして全てエラーになる)
...などなど弊害が発生します。
強力なモーターを積んで軽量化したミニ四駆がコーナーを勢いよく飛び出してしまった失敗経験
が全く役にたっていません。
対策例
-
チャンネル(channel)やセマフォ(semaphore)で同時実行数を制限する
-
エラーグループ(errgroup)とコンテキスト(context)でエラーが発生したら全部止まるようにしておく
-
Mutexで排他制御したりする
- スライスに結果を溜め込みたいだけなら
チャンネル(channel)にロック機能ついてるからmutexなどで無駄なロックをかけない
- スライスに結果を溜め込みたいだけなら
- (特にコネクト失敗などの)エラー時は
backoff等で間隔をあけてリトライ
する
実行環境のスペックや制限(OSのファイルオープン可能数、使えるメモリ容量、ディスクスペース、利用可能な i-node 数、通信回線の太さ、接続先の接続可能数...その他いろいろ)などを考慮しつつリミッター
を設定しバランスの良い平行数
で処理するようにしましょう。
まとめ
- Go言語の
特性を生かせるシーンで使うべき - C言語で fork してました、とか Java でスレッドやってました、と並列もしくは平行処理に慣れていないと、
goroutine は最初はきついかも -
ライブラリが充実していて楽チン
フォトクリエイトではGo言語バッチリです!
、AWS 任せてください〜
...な方からの ご応募をお待ちしております。