関数とパッケージ
読了目安 約5分
main に全部書かない理由から、関数分割の基準、パッケージ、命名規約、早期リターンまで、コードを分けて読みやすくする道具を学びます。
この章の目次
コードを分ける単位は 2 つ、小さいほうが関数、大きいほうがパッケージです。
main に全部書かない理由
競プロでは main に全部書いても困りませんが、本番のコードでは育つにつれて 3 つの点で行き詰まります。
- テストできない。Go の自動テスト(8 章で扱います)は、関数を単位に呼び出して結果を検証する仕組みです。
mainにしか処理がないと、検証のしようがありません。 - 再利用できない。同じ処理が別の場所で必要になったとき、関数になっていなければコピーして貼り付けるしかありません。すると、修正のたびにすべてのコピーを直して回ることになります。
- 読めない。数百行の
mainは一望できず、どこで何をしているかを探すだけで時間がかかります。
関数に分けると、名前がその部分の要約になります。
package main
import "fmt"
func isValidUserName(name string) bool {
if len(name) == 0 || len(name) > 20 {
return false
}
for _, r := range name {
isLower := 'a' <= r && r <= 'z'
isDigit := '0' <= r && r <= '9'
if !isLower && !isDigit {
return false
}
}
return true
}
func main() {
for _, name := range []string{"neziki", "Neziki", "", "go2024"} {
fmt.Println(name, "→", isValidUserName(name))
}
}main を読む人は、中身を読まなくても「ユーザー名を検証して表示している」とわかります。
関数分割の基準
行数の基準はなく、実用的な目安は次の 3 つです。
- 名前を付けられるまとまりであること。「ユーザー名を検証する」のように一言で言える処理は関数にできます。逆に、一言で言えないなら分け方を見直す合図です。
- 単体で動作を確かめたい処理であること。境界値(空文字列、上限ちょうどの長さ)で正しく動くか不安な処理は、関数にしておけば後からテストで検証できます。
- 2 回以上書きそうな処理であること。
ただし 1 行の処理にまで名前を付けると、読む人は定義を探して飛び回ることになります。
多値返却
Go の関数は、値を複数返せます。
package main
import "fmt"
func divmod(a, b int) (int, int) {
return a / b, a % b
}
func main() {
q, r := divmod(17, 5)
fmt.Println("商:", q, "余り:", r)
}これが効くのは、「結果」と「失敗したかどうか」を同時に返せるからです。 標準ライブラリの多くの関数が、次の形をしています。
func Atoi(s string) (int, error)この error の扱いは、5 章で学びます。
パッケージ
パッケージは、同じディレクトリに置かれた Go ファイルの集まりで、名前は各ファイル先頭の package 宣言で付けます。
同じパッケージ内なら別ファイルの関数や型も宣言なしで使え、外のものには import が必要です。
hello-web/
├── go.mod
├── main.go ← package main
└── user/
├── user.go ← package user
└── validate.go ← package usermain.go からは import "example.com/hello-web/user" と、モジュール名から始まるパスで取り込みます。
分け方の目安は「話題」で、ユーザーの処理は user、投稿の処理は post のように関心ごとにまとめます。
main が育ってまとまりが見えたら切り出せば十分です。
命名規約
覚える規約は 2 つです。
- エクスポート:名前をパッケージの外から使えるように公開すること。名前の 1 文字目を大文字にするだけでエクスポートされ、
isValidUserNameはパッケージ内専用、IsValidUserNameは外部公開です。publicやprivateのようなキーワードはありません。 - MixedCaps:複数の単語は
userNameやUserNameのように大文字で区切ってつなぎ、user_nameのようなアンダースコア区切りは使いません(コンパイルは通りますが、標準ライブラリも主要な外部パッケージも MixedCaps です)。URL や ID のような頭字語は、URLPath、userIDのように大文字のまま書きます。
早期リターンでネストを浅くする
素直に if と else を重ねると、こうなります。
func processOrder(userName string, quantity int) string {
if userName != "" {
if quantity > 0 {
if quantity <= 100 {
return "注文を受け付けました"
} else {
return "一度に注文できるのは 100 個までです"
}
} else {
return "個数が不正です"
}
} else {
return "ユーザー名が不正です"
}
}正常な処理が一番深い場所に埋まり、どの else がどの if に対応するかを覚えながら読むことになります。
早期リターンは、受け付けられない入力を先に検出して関数から抜け、正常な処理を一番外側に置く書き方です。
package main
import "fmt"
func processOrder(userName string, quantity int) string {
if userName == "" {
return "ユーザー名が不正です"
}
if quantity <= 0 {
return "個数が不正です"
}
if quantity > 100 {
return "一度に注文できるのは 100 個までです"
}
return "注文を受け付けました"
}
func main() {
fmt.Println(processOrder("neziki", 3))
fmt.Println(processOrder("", 3))
fmt.Println(processOrder("neziki", 500))
}上から順に「この条件なら、ここで終わり」と読み下せます。
5 章で学ぶ if err != nil の慣習も、この形の応用です。
試してみよう
次のプログラムは、テストの点数の合計と最高点を main の中で直接計算しています。
package main
import "fmt"
func main() {
scores := []int{72, 85, 91, 60, 78}
total := 0
for _, s := range scores {
total += s
}
best := scores[0]
for _, s := range scores {
if s > best {
best = s
}
}
fmt.Println("合計:", total)
fmt.Println("最高点:", best)
}これを sum と max の 2 つの関数に分けて、main を「slice の定義と Println 2 行」だけにしてください。
出力が変わらなければ成功です。
余裕があれば、平均を返す average 関数も追加してみてください。