Go: Goの関数:複数の戻り値、名前付き戻り値、可変引数、クロージャ、高階関数

関数はGoコードの「レゴブロック」のようなものです。Goは「ファーストクラス関数」という概念を極限まで追求しており、組み合わせプログラミングを簡単に行えるようにしています。

Goの関数設計哲学:最小限の構文で再利用性を最大化。複数の戻り値、名前付き戻り値、可変引数、クロージャ、関数の値渡し――他の言語では「フレームワーク」や「ライブラリ」を必要とするような機能も、Goではネイティブな構文の一部となっています。

1. 学習内容



2. データエンジニアの実体験

(1) 課題:Javaの関数は1つの値しか返せない

ボブはデータエンジニアで、最近、データ処理関数を記述する必要がありました:

「解析済みのユーザーオブジェクトとエラーコードの両方を返すparseUserData関数を書く必要がある。さらに、解析時間を追跡し、警告メッセージも返す必要がある。しかし、Javaでは戻り値は1つしか指定できないため、すべてを1つのResultオブジェクトに詰め込まざるを得ず、コードがスパゲッティのようにごちゃごちゃした状態になってしまっている。」

彼はJavaのコードを開いた:

JAVA
// Java: Can only return one value, so it must be packaged
public class ParseResult {
    public User user;
    public int errorCode;
    public long durationMs;
    public List<String> warnings;
}

public ParseResult parseUserData(String raw) {
    // All 5 return values are listed here
    return new ParseResult(...);
}

レビューの最中、同僚がこう不満を漏らした。「君の関数はまるでロシアのマトリョーシカみたいだ。5つのフィールドが必要になると、層を一つずつ剥がしていかなければならないんだ。」

(2) Go言語での解決策

Go言語は、複数の戻り値をネイティブにサポートしています:

GO
// user_parser.go
package main

import (
    "fmt"
    "strconv"
    "strings"
    "time"
)

// Multiple return values: user + errorCode + durationMs + warnings
func parseUserData(raw string) (User, int, time.Duration, []string) {
    start := time.Now()
    var warnings []string

    parts := strings.Split(raw, ",")
    if len(parts) != 3 {
        return User{}, 400, time.Since(start), []string{"Format error: 3 fields required"}
    }

    age, err := strconv.Atoi(parts[1])
    if err != nil {
        return User{}, 400, time.Since(start), []string{"Invalid age format"}
    }

    if age < 0 || age > 150 {
        warnings = append(warnings, "Age Anomaly")
    }

    user := User{Name: parts[0], Age: age, City: parts[2]}
    return user, 200, time.Since(start), warnings
}

type User struct {
    Name string
    Age  int
    City string
}

func main() {
    user, code, duration, warnings := parseUserData("Alice,28,Shanghai")

    fmt.Printf("Status: %d\n", code)
    fmt.Printf("User: %+v\n", user)
    fmt.Printf("Duration: %v\n", duration)
    fmt.Printf("Warnings: %v\n", warnings)
}

出力:

TEXT 📖 参照専用
Status: 200
User: {Name:Alice Age:28 City:Shanghai}
Duration: 12.5µs
Warnings: []

(3) パフォーマンス:Goの関数と他の言語との比較

特集 C Java Python Go
複数の戻り値 ❌ 構造体ラッパーが必要 ❌ ラッパークラスが必要 ✅ タプル ✅ ネイティブ
戻り値の名前 ✅ ネイティブ
関数としての値 関数ポインタ ラムダ 第一級オブジェクト ✅ 第一級オブジェクト
クロージャ ✅ (複雑) ラムダ ✅ 簡潔
可変長引数 ✅ (stdarg) ✅ varargs ✅ *args ✅ ...
💡 ヒント: Goでは複数の戻り値がサポートされているため、「return result + error」という記述が一般的な慣習となっています(func foo() (T, error))。これは、Goのエラー処理の考え方の基礎となっています。

100%
sequenceDiagram
    participant Caller
    participant Function as parseUserData()
    participant Parser as Internal Logic
    Caller->>Function: parseUserData(raw)
    Function->>Parser: Split fields
    Parser-->>Function: name, age, city
    Function-->>Caller: user + 200 + duration + warnings
    Note over Caller: Multiple return values: receive<br/>user/status/time/warnings simultaneously


3. func 関数の定義

(1) 関数定義の4つの要素

GO
func functionName(param1 type1, param2 type2) returnType {
    // Function Body
    return value
}
要素 キーワード 必須?
関数名 funcName はい
パラメータ一覧 (param type, ...) はい
戻り値の型 returnType なし(戻り値がない場合は省略可)
関数本体 { ... } はい

▶ サンプル:基本関数は4つの形式で定義される

GO
package main

import "fmt"

// Form 1: No parameters, no return value
func sayHello() {
    fmt.Println("Hello!")
}

// Form 2: With parameters but no return value
func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

// Form 3: With parameters and a return value
func add(a, b int) int {
    return a + b
}

// Form 4: Multiple Parameters and Multiple Return Values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

func main() {
    sayHello()                              // Hello!
    greet("Alice")                          // Hello, Alice!
    fmt.Println(add(2, 3))                  // 5
    result, err := divide(10.0, 2.0)
    fmt.Printf("%.2f, err=%v\n", result, err)  // 5.00, err=<nil>
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
Hello!
Hello, Alice!
5
5.00, err=<nil>

(3) パラメータの略語

同じ型の連続パラメータは、1つの型に統合することができます:

GO
func add(a, b int) int  // equivalent to a int, b int
func rect(w, h int) (int, int)  // both parameters are int


4. 複数の戻り値

(1) 複数の戻り値の定義

GO
package main

import "fmt"

func swap(a, b string) (string, string) {
    return b, a
}

func main() {
    x, y := swap("hello", "world")
    fmt.Println(x, y)  // world hello
}

▶ サンプル:値 + エラー(Goのシグネチャパターン)

GO
package main

import (
    "errors"
    "fmt"
)

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", errors.New("invalid id")
    }
    if id == 999 {
        return "", fmt.Errorf("user %d not found", id)
    }
    return fmt.Sprintf("User-%d", id), nil
}

func main() {
    user, err := findUser(1)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("Found: %s\n", user)

    // To ignore a return value: use _
    _, err = findUser(999)
    fmt.Printf("Ignored: err=%v\n", err)
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
Found: User-1
Ignored: err=user 999 not found

(3) 戻り値の命名

名前付き戻り値は関数の先頭で宣言されます。return ステートメントは、これらの変数を自動的に返します:

GO
package main

import "fmt"

func calc(a, b int) (sum, diff, product int) {
    sum = a + b
    diff = a - b
    product = a * b
    return  // bare return, automatically returns sum/diff/product
}

func main() {
    s, d, p := calc(10, 3)
    fmt.Printf("sum=%d, diff=%d, product=%d\n", s, d, p)
}

出力:

TEXT 📖 参照専用
sum=13, diff=7, product=30

(4) 複数の戻り値と名前付き戻り値

シナリオ 推奨事項
1つの値を返す 標準の戻り値
2つの値(値 + エラー)を返す 標準の戻り値
3つ以上の値を返す場合 返り値に名前を付ける(分かりやすくするため)
defer で戻り値を変更する必要があります 戻り値には名前を付ける必要があります
💡 ヒント: 戻り値に名前を付けると関数のシグネチャは長くなりますが、defer を使って戻り値を変更するには不可欠です(このレッスンの総合的な例で示されているように)。



5. 可変引数

(1) 可変引数構文 ...

GO
package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3))        // 6
    fmt.Println(sum(10, 20))         // 30
    fmt.Println(sum())               // 0

    // Break the slice apart and pass it in: nums...
    nums := []int{1, 2, 3, 4, 5}
    fmt.Println(sum(nums...))        // 15
}

出力:

TEXT 📖 参照専用
6
30
0
15

▶ サンプル:可変引数とフォーマット付き文字列

GO
package main

import "fmt"

// Similar to `fmt.Printf`: The first argument is fixed, and the rest are variable.
func logMessage(level string, args ...interface{}) {
    fmt.Printf("[%s] ", level)
    fmt.Println(args...)  // spread slice and pass to Println
}

func main() {
    logMessage("INFO", "Server started", "on port", 8080)
    logMessage("ERROR", "Database connection failed:", "timeout 5s")
    logMessage("DEBUG")
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
[INFO] Server started on port 8080
[ERROR] Database connection failed: timeout 5s
[DEBUG]

(3) 可変引数に関する制限

制限 説明
可変引数最大1つ func foo(a int, b ...int)
可変引数は最後に配置しなければならない func foo(a ...int, b int)
型は一貫している必要があります 複数の型を許可するには、...interface{} を使用してください


6. 匿名関数とクロージャ

(1) 匿名関数(関数リテラル)

匿名関数には名前がなく、変数に代入したり、直接呼び出したりすることができます:

GO
package main

import "fmt"

func main() {
    // Assign to a variable
    add := func(a, b int) int {
        return a + b
    }
    fmt.Println(add(2, 3))  // 5

    // Direct Call
    func(x int) {
        fmt.Printf("Anonymous function: x=%d\n", x)
    }(42)
}

出力:

TEXT 📖 参照専用
5
Anonymous function: x=42

(2) クロージャ:外部変数の捕捉

クロージャとは、関数とその関数が参照する外部変数の組み合わせのことです。クロージャにより、関数は自身が作成された環境を「記憶」することができます:

▶ サンプル:クロージャを用いたカウンタの実装

GO
package main

import "fmt"

// Returns a closure: increments by 1 on each call
func makeCounter() func() int {
    count := 0  // variable captured by closure
    return func() int {
        count++
        return count
    }
}

func main() {
    counter := makeCounter()
    fmt.Println(counter())  // 1
    fmt.Println(counter())  // 2
    fmt.Println(counter())  // 3

    // Each counter is an independent closure.
    another := makeCounter()
    fmt.Println(another())  // 1 (restarts counting)
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
1
2
3
1

(4) クロージャと通常の関数

次元 通常の関数 閉包
状態あり 状態なし 状態あり(変数のキャプチャ)
メモリ 静的 新しいインスタンスが作成されるたびに
ユースケース 純粋な計算 ファクトリ、デコレータ、コールバック


7. init 関数とパッケージの初期化

(1) init関数の特徴

機能 説明
引数なし、戻り値なし func init()
自動呼び出し パッケージがインポートされた際に自動的に実行される
複数 パッケージには複数の init 関数を定義できます(宣言された順に実行されます)
main の前 main() の実行前に実行される

▶ サンプル:パッケージの初期化(レジストリ)

GO
// registry.go
package main

import "fmt"

var registry = make(map[string]int)

func init() {
    registry["version"] = 1
    registry["max_connections"] = 100
    fmt.Println("[init] registry initialized")
}

func init() {
    registry["debug"] = 1
    fmt.Println("[init] debug enabled")
}

func main() {
    fmt.Printf("Registry: %+v\n", registry)
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
[init] registry initialized
[init] debug enabled
Registry: map[debug:1 max_connections:100 version:1]

(3) init と main の違い

関数 呼び出されるタイミング 目的
init() パッケージのインポート時に自動的に実行される グローバル変数を初期化し、ドライバを登録する
main() プログラムの起動時に実行される(1回) プログラムのエントリポイント


8. 引数および戻り値としての関数(高階関数)

Go言語の関数は第一級オブジェクトであり、変数に代入したり、引数として渡したり、値として返したりすることができます。

(1) 引数としての関数(コールバック)

GO
package main

import "fmt"

// The second parameter is the function type: it accepts an `int` and returns an `int`.
func process(nums []int, callback func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = callback(n)
    }
    return result
}

func double(n int) int { return n * 2 }
func square(n int) int { return n * n }

func main() {
    nums := []int{1, 2, 3, 4, 5}

    doubled := process(nums, double)
    fmt.Println("doubled:", doubled)

    squared := process(nums, square)
    fmt.Println("squared:", squared)
}

出力:

TEXT 📖 参照専用
doubled: [2 4 6 8 10]
squared: [1 4 9 16 25]

(2) 戻り値としての関数(ファクトリ)

GO
package main

import "fmt"

func makeAdder(x int) func(int) int {
    return func(y int) int {
        return x + y
    }
}

func main() {
    add10 := makeAdder(10)
    add100 := makeAdder(100)

    fmt.Println(add10(5))     // 15
    fmt.Println(add100(5))    // 105
}

出力:

TEXT 📖 参照専用
15
105

▶ サンプル:関数型と高階関数(map/filter/reduce)

GO
package main

import "fmt"

func mapFunc(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = f(n)
    }
    return result
}

func filterFunc(nums []int, predicate func(int) bool) []int {
    var result []int
    for _, n := range nums {
        if predicate(n) {
            result = append(result, n)
        }
    }
    return result
}

func reduceFunc(nums []int, initial int, f func(int, int) int) int {
    acc := initial
    for _, n := range nums {
        acc = f(acc, n)
    }
    return acc
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // 1. Map: Each element *2
    doubled := mapFunc(nums, func(n int) int { return n * 2 })

    // 2. Filter: Keep even numbers
    evens := filterFunc(nums, func(n int) bool { return n%2 == 0 })

    // 3. Reduce: Sum
    sum := reduceFunc(nums, 0, func(acc, n int) int { return acc + n })

    fmt.Printf("doubled: %v\n", doubled)
    fmt.Printf("evens: %v\n", evens)
    fmt.Printf("sum: %d\n", sum)
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
doubled: [2 4 6 8 10 12 14 16 18 20]
evens: [2 4 6 8 10]
sum: 55


9. 完全な例:データ処理パイプライン

この関数のすべての機能を組み合わせて、ETL(抽出・変換・ロード)データパイプラインを構築します:

GO
// pipeline.go
package main

import (
    "fmt"
    "strings"
    "time"
)

// Data Sources
func extract() []string {
    return []string{
        "  Alice,28,Shanghai  ",
        "Bob,32,Beijing",
        "CHARLIE,45,Guangzhou",
        "",  // empty data
        "Dave,abc,ErrorCity",  // anomalous data
    }
}

// Step 1: Remove Spaces + Split Fields
func trim(s string) []string {
    return strings.Split(strings.TrimSpace(s), ",")
}

// Step 2: Convert to uppercase
func upper(s []string) []string {
    for i, v := range s {
        s[i] = strings.ToUpper(v)
    }
    return s
}

// Step 3: Verify the number of fields
func validate(s []string) (string, bool) {
    if len(s) != 3 || s[0] == "" {
        return "", false
    }
    return strings.Join(s, "|"), true
}

// Pipeline Function: Combining 3 Steps + Defer Report
func processPipeline(name string, data []string) (valid int, errors int) {
    defer func() {
        // Name the return value so that `defer` can modify the result
        fmt.Printf("[%s] Completed: valid=%d errors=%d duration=%v\n",
            name, valid, errors, time.Since(startTime))
    }()

    for _, raw := range data {
        s := trim(raw)
        if len(s) < 3 {
            errors++
            continue
        }
        s = upper(s)
        if result, ok := validate(s); ok {
            valid++
            fmt.Printf("  -> %s\n", result)
        } else {
            errors++
        }
    }
    return valid, errors
}

var startTime = time.Now()

func main() {
    fmt.Println("=== Start of Data Pipeline ===")

    // Combining Steps Using Functions as Arguments
    data := extract()
    valid, errors := processPipeline("ETL-1", data)

    fmt.Printf("\nSummary: valid=%d, errors=%d\n", valid, errors)
}

期待される出力:

TEXT 📖 参照専用
=== Start of Data Pipeline ===
  -> ALICE|28|SHANGHAI
  -> BOB|32|BEIJING
  -> CHARLIE|45|GUANGZHOU
  -> DAVE|ABC|ERRORCITY
[ETL-1] Completed: valid=4 errors=1 duration=2.5µs

Summary: valid=4, errors=1
🔥 よくある間違い: startTime はファイルレベルで宣言する必要があります(init 内で := を使用することはできません)。これが、init関数とvar宣言の微妙な違いです。つまり、varはパッケージレベルであるのに対し、initは関数レベルです。


❓ よくある質問

Q Goの関数はオーバーロードに対応していますか?
A いいえ、対応していません。Goは、C++やJavaに見られる「オーバーロードの曖昧さ」による複雑さを避けるため、意図的に関数のオーバーロードを導入していません。同様の動作が必要な場合は、可変引数 func foo(a ...int) または異なる関数名を使用してください。
Q 複数の戻り値と構造体の返却にはどのような違いがありますか?
A 複数の戻り値は、2~3つの値(特に値とエラー)を扱う場合に適しており、コードをより簡潔にできます。3つ以上の値の場合は、構造体を使用することをお勧めします。構造体にはフィールド名があるため、可読性が向上するからです。
Q 戻り値に名前を付けることの実用的な利点は何ですか?
A (1) 自動生成される return ステートメントによりコード量が削減されます;(2) defer で戻り値を変更できます(重要!これはこのレッスンの総合的な例題で使用されています);(3) ドキュメント化――関数のシグネチャがドキュメントの役割を果たします。
Q クロージャはメモリリークを引き起こすことがありますか?
A はい。クロージャによってキャプチャされた変数の存続期間は、クロージャ自体が終了するまで延長されます。クロージャを長時間保持すると(キャッシュ内やゴルーチン内など)、それらの変数がガベージコレクタによって回収されなくなります。
Q init 関数は手動で呼び出すことができますか?
A いいえ。init は Go ランタイムによって自動的にのみ呼び出され、各パッケージの init は 1 回だけ実行されます。init を手動で呼び出そうとすると、コンパイルエラーが発生します。
Q 可変引数とスライス引数の違いは何ですか?
A 可変引数 nums ...int は、本質的には関数内部の []int スライスですが、呼び出し規約が異なります:foo(1,2,3)foo([]int{1,2,3}...)。前者は構文上の簡略化です。
Q 関数を値として使用すると、パフォーマンスの低下を招きますか?
A 最近のコンパイラは関数の値を(直接呼び出すことで)最適化するため、オーバーヘッドは事実上ありません。ただし、関数をマップやスライスに格納して頻繁に呼び出すと、間接アドレス指定によるわずかなオーバーヘッドが生じますが、通常は無視できる程度です。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐):すべての引数のうち最大の値を返す max(nums ...int) int 関数を記述してください。この関数は可変長引数を使用する必要があります。また、max(1, 5, 3, 9, 2) を呼び出した際に 9 が出力されるようにしてください。

  2. 応用問題(難易度 ⭐⭐):銀行口座をシミュレートする makeBankAccount(initial int) (deposit func(int) int, withdraw func(int) (int, bool), balance func() int) を実装してください。deposit は残高を増やし、新しい残高を返します。withdraw は資金を引き落とします(残高が不足している場合は false を返します)。balance は残高を照会します。クロージャを使用する必要があります。

  3. チャレンジ問題(難易度 ⭐⭐⭐)関数型データパイプラインを実装してください。入力 []string(数字の文字列)に対し、高階関数を使用して 4 つのステップ parse -> filter(>10) -> map(*2) -> sum を順次実行してください。各ステップは独立した関数である必要があり、これらの関数を組み合わせることで最終結果が得られるようにしてください。例:["1", "15", "3", "20"]parse [1,15,3,20]filter>10 [15,20]map*2 [30,40]sum 70

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%