インターンを開催して分かったGolangを書き始める人に知っておいてほしい事
こんにちは。エウレカ APIチームの小島です。
先日エウレカではサマーインターンを開催しました 。今回の課題は「マッチングサービスに必要なAPIをGolangで実装する」という内容でした。
※ 詳細については高橋の記事[エウレカサマーインターンを開催しました! エウレカサマーインターンを開催しました!(サーバーサイド編) にあるので割愛します。
課題は当日発表だったこともあり、その日からGolangを始めた学生も少なくありませんでしたが、苦戦しつつも皆さん課題に一生懸命取り組んでいました。
本記事では、初心者が知っておきたいGolangの扱い方など今回のサマーインターンの課題の内容をふまえつついくつか紹介します。
基本的な書き方
1. Golangでは基本的にキャメルケースを使うことが推奨されています。もちろんスネークケースでも書けますが、Golintをかけるとすべて指摘されるので書く前にこれだけは覚えておきましょう。
2. 頭文字を大文字にするとpublicな変数、小文字にするとprivateな変数になります。
3. 命名もスコープも短くわかりやすく書くことを意識しましょう。スコープを短くすることで変数の名前が短くとも可読性を損なわず書くことができます。エラーハンドリングの場合など下記の様に書くとerrのスコープはif内のみになるので最小限で済みます。
// 例:
if err := Update(&user); err != nil {
return err
}
4. elseはできるだけ書かないように心がけましょう。ここは他の言語でも同様に言える部分ではあると思うのですが、Golangの場合はよりelseを使わないような書き方が良いと言われることが多いです。ネストを浅くできたりとコードをシンプルに書けます。例えば以下のようなコードがあったとします。
user, err := GetUserByID(id)
if err == nil {
if user == nil {
return nil, errors.New("not found")
}
return user, nil
} else {
return nil, err
}
このような場合は以下のように書きかえられます。
user, err := GetUserByID(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("not found")
}
return user, nil
これだけでerrがなかった場合の処理もネストを浅くでき、可読性が上がったと思います。条件分岐内の処理がreturnやbreakなどで終わる場合は上記のように順番を整理してみるなどしてみましょう。
5. switchにはcase毎に条件をつけられます。
switch {
case a < b:
fmt.Println(“aはbより小さい”)
case a > b:
fmt.Println(“aはbより大きい”)
case a == b:
fmt.Println(“aはbと同じ”)
}
Golangの場合case内にbreakがなくとも下の処理は実行されません。また、case文にはカンマ区切りで複数条件を並べて書けます。
switch typ {
case “A”, “B”, “C”:
fmt.Println(typ)
}
分岐の書き方によって可読性は大きく変わるので、条件によって使い分けるようにしましょう。
スライス
1. スライスを初期化する際はmakeを使用することでよりパフォーマンスを上げることができます。
単純にスライスに値を追加する処理を書いた場合以下のように書けます。
// 例: ユーザーエンティティのスライスからidのスライスを生成する処理
var ids []int64
for _, user := range users {
ids = append(ids, user.ID)
}
上記のコードでも処理上問題はないのですが、以下のように書くとより実行速度を上げることができます。
// 例: ユーザーエンティティのスライスからidのスライスを生成する処理
ids := make([]int64, len(users))
for i, user := range users {
ids[i] = user.ID
}
スライスにはlengthとcapacityがあります。makeを使うことで予めそれらを指定することができます。詳細には今回割愛しますが、簡単に説明してしまうとfor文内で毎回値を入れるための箱と箱を置く場所を用意してから今あるスライスにつなぎこんで値をいれていくのと、最初に必要なだけの箱と場所を用意しておいて後はそこに値を入れていくだけなので、後者のほうが速くなるわけです。
注意点としてはでmakeで長さを指定した場合、すでに長さ分初期値が入った状態のためappendしてしまうとその後ろに値が追加されてしまいます。
// スライス
ids := make([]int64, 10)
fmt.Println(“value: “, ids)
// value: [0 0 0 0 0 0 0 0 0 0]
fmt.Println(“length: “, len(ids))
// length: 10
ids = append(ids, 99)
fmt.Println(“value: “, ids)
// value: [0 0 0 0 0 0 0 0 0 0 99]
// 一番うしろに追加されてしまう
fmt.Println(“length: “, len(ids))
// length: 11
また、長さが決まっていないが最大の長さがわかっている場合はcapacityを指定することでappendを速くすることができます。
ids := make([]int64, 0, len(users)) // capacityだけ指定
for i, user := range users {
// 男性が混じっていた場合は返さない
if user.Gender == “男性” {
continue
}
ids = append(ids, user.ID)
}
こちらも簡単に説明するなら、先程の説明にあった長さと場所の確保の場所の確保だけ最初にしてしまい、あとから箱だけ追加するだけなので通常よりも速くすることができます。
N+1問題の解消
ループ中にSQLクエリが実行されてしまい、その繰り返し回数だけクエリが発行されてしまうのは基本的にパフォーマンスの面でよくありません。これはN+1問題と呼ばれています。今回の課題で用意したのは上記のような問題が起きやすいようなテーブル構成でした。N+1問題を解決する方法はいくつかありますが、今回はmapを使う方法を説明します。
// 例 いいねをしたお相手一覧を取得する
// ユーザーの使ったいいねのデータを取得
likes, _ := FindLikesByUserID(meID)
responses := make([]UserResponse, len(likes))
for i, like := range likes {
r := UserResponse{}
partnerID := like.PartnerID
// partnerIDからお相手の情報を取得
// select * from `user` where `id` = [partnerID]
p, _ := FindUserByID(partnerID)
r.ID = p.ID
r.Name = p.Name
// お相手の画像を取得
// select * from `user_image` where `user_id` = [partnerID]
image, _ := FindUserImageByUserID(partnerID)
r.ImagePath = image.Path
responses[i] = r
}
上記の例はいいねを送ったお相手の一覧を取得するため、いいねの情報から送ったお相手のプロフィール情報と画像を取得してレスポンス形式に直す処理です。
送ったいいねの数だけお相手の情報を取得するSQLクエリが発生してしまいます。送った相手が増えるとクエリの数も同じだけ増えます。これを解消するため以下のように修正します。
// ユーザーの使ったいいねのデータを取得
likes, _ := FindLikesByUserID(meID)
partnerIDs := make([]int64, len(likes))
for i, like := range likes {
partnerIDs[i] = like.PartnerID
}
// お相手の情報を取得
// select * from `user` where `id` in ([partnerIDs])
partners, _ := FindUsersByIDs(partnerIDs)
partnerMap := map[int64]User{}
for _, p := range partners {
partnerMap[p.ID] = p
}
// お相手の画像を取得
// select * from `user_image` where `user_id` in ([partnerIDs])
images, _ := FindUserImagesByUserIDs(partnerIDs)
partnerImageMap := map[int64]UserImage{}
for _, image := range images {
partnerImageMap[image.UserID] = image
}
responses := make([]UserResponse, len(partnerIDs))
for i, id := range partnerIDs {
r := UserResponse{}
if p, ok := partnerMap[id]; ok {
r.ID = p.ID
r.Name = p.Name
}
if image, ok := partnerImageMap[id]; ok {
r.ImagePath = image.Path
}
responses[i] = r
}
処理の内容を簡単にまとめると
- 取得したlikes からお相手のidのスライスを生成 — partnerIDs
- お相手の情報、お相手の画像をそれぞれ partnerIDsでまとめて取得
- 取得したスライスをお相手のIDをキーにしてmapに展開
- レスポンス生成時にpartnerIDからそれぞれの構造体を取得
- レスポンススライスにいれる
のようにしています。コードは長くなりましたが、これでN+1問題が解消されました。通常mapのインデックスアクセスはsliceよりは遅いですが、map内の要素数が増えたとしても一定したアクセス速度を出すことができます。
おわりに
今回はサマーインターンの課題からいくつかポイントをピックアップして書きました。これからGolangを書く方の参考になればと思います。
エウレカではGolangを書きたい方を募集しています。夏に限らずインターンも随時募集しておりますのでご興味のある方はぜひご応募ください。