🔩 ねじき教室 Go と Web の教室

関数とパッケージ

読了目安 約5分

main に全部書かない理由から、関数分割の基準、パッケージ、命名規約、早期リターンまで、コードを分けて読みやすくする道具を学びます。

この章の目次

コードを分ける単位は 2 つ、小さいほうが関数、大きいほうがパッケージです。

main に全部書かない理由

競プロでは main に全部書いても困りませんが、本番のコードでは育つにつれて 3 つの点で行き詰まります。

  • テストできない。Go の自動テスト(8 章で扱います)は、関数を単位に呼び出して結果を検証する仕組みです。main にしか処理がないと、検証のしようがありません。
  • 再利用できない。同じ処理が別の場所で必要になったとき、関数になっていなければコピーして貼り付けるしかありません。すると、修正のたびにすべてのコピーを直して回ることになります。
  • 読めない。数百行の main は一望できず、どこで何をしているかを探すだけで時間がかかります。

関数に分けると、名前がその部分の要約になります。

Go
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 の関数は、値を複数返せます。

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)
}

これが効くのは、「結果」と「失敗したかどうか」を同時に返せるからです。 標準ライブラリの多くの関数が、次の形をしています。

Go
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 user

main.go からは import "example.com/hello-web/user" と、モジュール名から始まるパスで取り込みます。

分け方の目安は「話題」で、ユーザーの処理は user、投稿の処理は post のように関心ごとにまとめます。 main が育ってまとまりが見えたら切り出せば十分です。

命名規約

覚える規約は 2 つです。

  • エクスポート:名前をパッケージの外から使えるように公開すること。名前の 1 文字目を大文字にするだけでエクスポートされ、isValidUserName はパッケージ内専用、IsValidUserName は外部公開です。publicprivate のようなキーワードはありません。
  • MixedCaps:複数の単語は userNameUserName のように大文字で区切ってつなぎ、user_name のようなアンダースコア区切りは使いません(コンパイルは通りますが、標準ライブラリも主要な外部パッケージも MixedCaps です)。URL や ID のような頭字語は、URLPathuserID のように大文字のまま書きます。

早期リターンでネストを浅くする

素直に ifelse を重ねると、こうなります。

Go
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 に対応するかを覚えながら読むことになります。

早期リターンは、受け付けられない入力を先に検出して関数から抜け、正常な処理を一番外側に置く書き方です。

Go
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 の中で直接計算しています。

Go
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)
}

これを summax の 2 つの関数に分けて、main を「slice の定義と Println 2 行」だけにしてください。 出力が変わらなければ成功です。 余裕があれば、平均を返す average 関数も追加してみてください。