関数とパッケージ
読了目安 約5分
main に全部書かない理由から、関数分割の基準、パッケージ、命名規約、早期リターンまで、コードを分けて読みやすくする道具を学びます。
- 入力を読む 関数
- main 組み立てる
- 結果を返す 関数
この章の目次
コードを分ける単位は 2 つ、小さいほうが関数、大きいほうがパッケージです。
main に全部書かない理由
競プロでは main に全部書いても困りませんが、本番のコードでは育つにつれて 3 つの点で行き詰まります。
- テストできない。テスト(10 章で扱います)は、競プロで提出前にサンプルケースと出力を見比べる作業を、自動化して恒久化したものです。関数を単位に呼び出して結果を検証する仕組みなので、
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 の扱いは、6 章で学びます。
パッケージ
パッケージは、同じディレクトリに置かれた Go ファイルの集まりで、名前は各ファイル先頭の package 宣言で付けます。
同じパッケージ内なら別ファイルの関数や型も宣言なしで使え、外のものには import が必要です。
hello-web/
├── go.mod
├── main.go ← package main
└── user/
├── user.go ← package user
└── validate.go ← package usermain.go からは、パスで取り込みます。モジュール名から始める必要があります。
たとえば、main.go から user パッケージを取り込むには、1 章で go mod init example.com/hello-web としたので、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))
}上から順に「この条件なら、ここで終わり」と読み下せます。
6 章で学ぶ 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 関数も追加してみてください。
答えを見る
package main
import "fmt"
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func max(nums []int) int {
best := nums[0]
for _, n := range nums {
if n > best {
best = n
}
}
return best
}
func main() {
scores := []int{72, 85, 91, 60, 78}
fmt.Println("合計:", sum(scores))
fmt.Println("最高点:", max(scores))
}合計: 386
最高点: 91average は、割り算の前に float64 へ変換すると小数まで出ます。
func average(nums []int) float64 {
return float64(sum(nums)) / float64(len(nums))
}fmt.Println("平均:", average(scores)) を足すと、平均: 77.2 と表示されます。