📚 背景知識(読んでから問題へ)
Goの関数は他の多くの言語と違い、複数の値を戻り値として直接返せます。タプルやカスタムクラスを作らなくても、カンマ区切りでそのまま複数の値を返せるのがGoの特徴です。
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
この「複数戻り値」は、Goのエラーハンドリングの根幹を支えています。例外機構を持たないGoでは、(結果, error) という形で「正常な結果」と「失敗したかどうか」を同じレベルの戻り値として明示的に返すのが標準パターンです。呼び出し側は必ず result, err := divide(10, 2) のように受け取り、if err != nil で分岐します。これにより「エラーが発生しうる」という事実が関数シグネチャに刻まれ、呼び出し側が見落としにくくなります。
戻り値には名前を付けることもできます(named return values)。
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("division by zero")
return
}
result = a / b
return
}
return だけを書く「裸のreturn(naked return)」は、名前付き戻り値の現在の値をそのまま返します。短い関数では便利ですが、長い関数で使うと「どの値が返るのか」が読みにくくなるため、Goコミュニティでは短い関数に限定して使うのが慣例です。
もう一つの重要な機能が可変長引数(variadic parameters)です。
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
...int は「0個以上のint」を受け取れることを意味し、関数内部ではnumsは通常の[]intスライスとして扱われます。呼び出し側はsum(1, 2, 3)のように個別の値を渡すことも、sum(nums...)(スライス名の後ろに...を付ける)のように既存のスライスを展開して渡すこともできます。標準ライブラリのfmt.Println(a ...interface{})やappend(s []T, vs ...T)はまさにこの仕組みを使っています。
📝 問題
以下の3つの関数を実装してください。
要件:
func divide(a, b float64) (float64, error)を実装する。bが0の場合は(0, error)を返す(エラーメッセージは"division by zero"とする)。それ以外はa / bの結果とnilを返すfunc sum(nums ...int) intを実装する。可変長引数で受け取った整数の合計を返す。引数が0個の場合は0を返すfunc average(nums ...int) (float64, error)を実装する。可変長引数で受け取った整数の平均値(float64)を返す。引数が0個の場合は(0, error)を返す(エラーメッセージは"no numbers provided"とする)main関数で以下をすべて実行し、結果を表示することdivide(10, 2)の成功ケースdivide(10, 0)のエラーケース(err != nilを確認して表示)sum(1, 2, 3, 4, 5)の呼び出しaverage()の引数なしエラーケースaverage(1, 2, 3)の成功ケース- 既存のスライス
nums := []int{10, 20, 30}をsum(nums...)の形で展開して渡す呼び出し
go run main.go でそのまま実行できる、完全な1つのプログラムとして提出してください。
🔍 ヒント(段階的開示)
ヒント1 — 方向性
(float64, error)のように戻り値の型を丸括弧内にカンマ区切りで並べると、複数の値を返す関数を宣言できます。呼び出し側もa, b := f()のように複数の変数で同時に受け取ります- 可変長引数は仮引数リストの最後に1つだけ書けて、
...型名という構文になります
ヒント2 — アプローチ
if b == 0 {
return 0, errors.New("division by zero")
}
errors.New(文字列)はerrorインターフェースを満たす値を1行で作れる標準ライブラリの関数です。averageはsumを再利用してsum(nums...) / float64(len(nums))のように書けますが、len(nums) == 0の場合はゼロ除算になる前に早期リターンでエラーを返す必要があります。
ヒント3 — コード骨格
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func sum(nums ...int) int {
total := 0
for _, n := range nums {
// ...
}
return total
}
func average(nums ...int) (float64, error) {
if len(nums) == 0 {
return 0, errors.New("no numbers provided")
}
// sum(nums...) を使って平均を計算する
}
func main() {
// divide, sum, average をそれぞれ呼び出して表示
}
✅ 模範解答
package main
import (
"errors"
"fmt"
)
// divide はaをbで割った結果を返す。bが0の場合はエラーを返す。
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// sum は可変長引数で受け取った整数の合計を返す。
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// average は可変長引数で受け取った整数の平均値を返す。
// 引数が0個の場合はエラーを返す。
func average(nums ...int) (float64, error) {
if len(nums) == 0 {
return 0, errors.New("no numbers provided")
}
return float64(sum(nums...)) / float64(len(nums)), nil
}
func main() {
// 1. divide の成功ケース
result, err := divide(10, 2)
if err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Printf("divide(10, 2) = %.1f\n", result)
}
// 2. divide のエラーケース
result, err = divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Printf("divide(10, 0) = %.1f\n", result)
}
// 3. sum の呼び出し(個別の値を渡す)
fmt.Println("sum(1, 2, 3, 4, 5) =", sum(1, 2, 3, 4, 5))
// 4. average の引数なしエラーケース
avg, err := average()
if err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Printf("average() = %.2f\n", avg)
}
// 5. average の成功ケース
avg, err = average(1, 2, 3)
if err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Printf("average(1, 2, 3) = %.2f\n", avg)
}
// 6. 既存スライスを展開して渡す
nums := []int{10, 20, 30}
fmt.Println("sum(nums...) =", sum(nums...))
}
▶ 実行結果を見る(go run main.go)
divide(10, 2) = 5.0
エラー: division by zero
sum(1, 2, 3, 4, 5) = 15
エラー: no numbers provided
average(1, 2, 3) = 2.00
sum(nums...) = 60
※ この実行結果は実際に go run main.go で検証済みです。
🪜 Step-by-Step 解説
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
戻り値の型を(float64, error)と丸括弧内にカンマ区切りで宣言すると、return文で2つの値を同時に返せます。エラーがない場合はerrorの位置にnilを返すのがGoの一貫した慣例です。呼び出し側は必ず両方の戻り値を受け取る必要があります(片方だけを受け取ることはできません)。
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
nums ...intと宣言された仮引数は、関数の内部では通常の[]intとして扱えます。そのためrangeでそのままループできます。呼び出し側が引数を1つも渡さなかった場合、numsは長さ0の空スライス(nilスライス)になり、for rangeは0回ループするためtotalは0のまま返ります。
func average(nums ...int) (float64, error) {
if len(nums) == 0 {
return 0, errors.New("no numbers provided")
}
return float64(sum(nums...)) / float64(len(nums)), nil
}
len(nums) == 0のチェックを先頭に置くことで、後続の割り算処理(0での除算)に到達する前にエラーで打ち切ります。これは「異常系を先に処理して早く関数を抜ける」というGoで好まれるスタイル(ガード節・early return)です。sum(nums...)は既存の関数を呼び出し、...でスライスを展開して渡しています。
...でスライスを展開して渡すnums := []int{10, 20, 30}
sum(nums...)
sum(nums)と書くと「[]intという1つの値を渡そうとしている」とコンパイルエラーになります(sumは...int = []intを受け取りますが、暗黙の変換はされません)。nums...と末尾に...を付けることで、「このスライスの要素を可変長引数として展開して渡す」という意図を明示します。
💡 設計思想・なぜこう書くのか
Optional・Result型・nullなどの仕組みが必要になります。Goは代わりに「戻り値を複数返せる」というシンプルな言語機能だけで(result, error)パターンを実現しており、コードを読むだけでエラー処理の有無が分かるのがこの設計の狙いです。[]Tとして扱われるため、新しい概念を追加せずに「スライスの知識だけで理解できる」機能になっている点もGoらしい設計です。🌐 他言語との比較
| 観点 | Go | Java | Python | TypeScript |
|---|---|---|---|---|
| 複数戻り値 | 言語機能として標準サポート((T1, T2)) | 非サポート。専用クラスやMap、Optionalで代用 | タプル(a, b)で疑似的に実現 | タプル型[T1, T2]で疑似的に実現 |
| エラー表現 | 戻り値のerrorを明示的にチェック | 例外(try/catch) | 例外(try/except) | 例外(try/catch)またはResult型(ライブラリ依存) |
| 可変長引数 | ...T(内部は[]T) | T... args(内部は配列) | *args(内部はタプル) | ...args: T[](内部は配列) |
| スライス/配列の展開渡し | f(slice...) | 配列をそのまま渡せる | f(*list) | f(...array) |
JavaのOptional<T>やPythonのタプルは「複数の情報をまとめて返す」目的では似た働きをしますが、Goの複数戻り値は言語構文レベルで組み込まれているため、ラッパー型を作る手間がありません。特にJava/TypeScript経験者は「例外を投げずにエラーを戻り値として返す」設計に最初は冗長に感じやすいですが、try/catchのようにどこで捕捉されるか分からない制御フローと違い、エラー処理の分岐が呼び出し箇所ごとに必ず目に見えるという利点があります。
🏆 実務での使いどころ
(結果, error)パターン: ファイル読み込み・DB問い合わせ・HTTPリクエストなど、失敗しうるあらゆる処理でこのパターンが使われます。標準ライブラリのos.Open、strconv.Atoiなども同じ形ですfmt.Println/fmt.Sprintf系の可変長引数: ログ出力・フォーマット関数を自作する際、任意個数の値を受け取る...interface{}(Go 1.18以降は...any)は頻出パターンです- オプション引数の代替: Goにはデフォルト引数がないため、
func NewServer(addr string, opts ...Option)のように可変長引数+Functional Optionsパターンで「オプション引数」を実現する設計がライブラリ設計で広く使われます - バリデーション関数の集約:
func validate(rules ...Rule) errorのように、複数のチェックルールをまとめて渡し、最初に失敗したエラーを返す設計にも使われます
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
result := divide(10, 2)のように片方の戻り値だけ受け取ろうとしてコンパイルエラーになる | 他言語で戻り値を無視できる感覚を持ち込む | Goでは複数戻り値をすべて受け取る必要がある。不要な値は_(ブランク識別子)で明示的に捨てる(result, _ := divide(10, 2)) |
sum(nums)のようにスライスをそのまま渡してコンパイルエラーになる | 可変長引数がスライスと同じ型に見えるため、暗黙変換できると錯覚する | 可変長引数にスライスを渡すにはnums...と明示的に展開する必要がある |
可変長引数を関数の途中に置こうとしてコンパイルエラーになる(例: func f(a ...int, b string)) | 他言語の可変長引数の位置ルールと混同する | Goでは可変長引数は仮引数リストの最後に1つだけしか置けない |
| 名前付き戻り値の裸のreturnを長い関数で多用し、どの値が返るか読みにくくなる | 短く書けて便利に見える | 裸のreturnは数行程度の短い関数に限定し、長い関数では値を明示してreturn result, errと書く |
🚀 次のステップ
- 発展:
average関数を、標準ライブラリを使わずに整数オーバーフローを気にせず実装する方法(sumの戻り値をintのまま保持し最後にfloat64へ変換する現在の実装と、ループ内で都度float64に変換して加算していく実装の違い)を考えてみましょう - 次回予告: Day 008 — 構造体の基礎(struct定義・フィールドタグ、実装問題)