Go: Goの関数:複数の戻り値、名前付き戻り値、可変引数、クロージャ、高階関数
関数はGoコードの「レゴブロック」のようなものです。Goは「ファーストクラス関数」という概念を極限まで追求しており、組み合わせプログラミングを簡単に行えるようにしています。
Goの関数設計哲学:最小限の構文で再利用性を最大化。複数の戻り値、名前付き戻り値、可変引数、クロージャ、関数の値渡し――他の言語では「フレームワーク」や「ライブラリ」を必要とするような機能も、Goではネイティブな構文の一部となっています。
1. 学習内容
func関数の定義における 4 つの要素- 複数の戻り値と名前付き戻り値
- 変数引数
...の使用 - 匿名関数とクロージャ
init関数およびパッケージの初期化- 引数および戻り値としての関数(高階関数)
- 関数を用いたデータ処理パイプラインの構築
2. データエンジニアの実体験
(1) 課題:Javaの関数は1つの値しか返せない
ボブはデータエンジニアで、最近、データ処理関数を記述する必要がありました:
「解析済みのユーザーオブジェクトとエラーコードの両方を返す
parseUserData関数を書く必要がある。さらに、解析時間を追跡し、警告メッセージも返す必要がある。しかし、Javaでは戻り値は1つしか指定できないため、すべてを1つのResultオブジェクトに詰め込まざるを得ず、コードがスパゲッティのようにごちゃごちゃした状態になってしまっている。」
彼は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言語は、複数の戻り値をネイティブにサポートしています:
// 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)
}
出力:
Status: 200
User: {Name:Alice Age:28 City:Shanghai}
Duration: 12.5µs
Warnings: []
(3) パフォーマンス:Goの関数と他の言語との比較
| 特集 | C | Java | Python | Go |
|---|---|---|---|---|
| 複数の戻り値 | ❌ 構造体ラッパーが必要 | ❌ ラッパークラスが必要 | ✅ タプル | ✅ ネイティブ |
| 戻り値の名前 | ❌ | ❌ | ❌ | ✅ ネイティブ |
| 関数としての値 | 関数ポインタ | ラムダ | 第一級オブジェクト | ✅ 第一級オブジェクト |
| クロージャ | ✅ (複雑) | ラムダ | ✅ | ✅ 簡潔 |
| 可変長引数 | ✅ (stdarg) | ✅ varargs | ✅ *args | ✅ ... |
func foo() (T, error))。これは、Goのエラー処理の考え方の基礎となっています。
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つの要素
func functionName(param1 type1, param2 type2) returnType {
// Function Body
return value
}
| 要素 | キーワード | 必須? |
|---|---|---|
| 関数名 | funcName |
はい |
| パラメータ一覧 | (param type, ...) |
はい |
| 戻り値の型 | returnType |
なし(戻り値がない場合は省略可) |
| 関数本体 | { ... } |
はい |
▶ サンプル:基本関数は4つの形式で定義される
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>
}
出力:
Hello!
Hello, Alice!
5
5.00, err=<nil>
(3) パラメータの略語
同じ型の連続パラメータは、1つの型に統合することができます:
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) 複数の戻り値の定義
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のシグネチャパターン)
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)
}
出力:
Found: User-1
Ignored: err=user 999 not found
(3) 戻り値の命名
名前付き戻り値は関数の先頭で宣言されます。return ステートメントは、これらの変数を自動的に返します:
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)
}
出力:
sum=13, diff=7, product=30
(4) 複数の戻り値と名前付き戻り値
| シナリオ | 推奨事項 |
|---|---|
| 1つの値を返す | 標準の戻り値 |
| 2つの値(値 + エラー)を返す | 標準の戻り値 |
| 3つ以上の値を返す場合 | 返り値に名前を付ける(分かりやすくするため) |
defer で戻り値を変更する必要があります |
戻り値には名前を付ける必要があります |
defer を使って戻り値を変更するには不可欠です(このレッスンの総合的な例で示されているように)。
5. 可変引数
(1) 可変引数構文 ...
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
}
出力:
6
30
0
15
▶ サンプル:可変引数とフォーマット付き文字列
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")
}
出力:
[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) 匿名関数(関数リテラル)
匿名関数には名前がなく、変数に代入したり、直接呼び出したりすることができます:
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)
}
出力:
5
Anonymous function: x=42
(2) クロージャ:外部変数の捕捉
クロージャとは、関数とその関数が参照する外部変数の組み合わせのことです。クロージャにより、関数は自身が作成された環境を「記憶」することができます:
▶ サンプル:クロージャを用いたカウンタの実装
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)
}
出力:
1
2
3
1
(4) クロージャと通常の関数
| 次元 | 通常の関数 | 閉包 |
|---|---|---|
| 状態あり | 状態なし | 状態あり(変数のキャプチャ) |
| メモリ | 静的 | 新しいインスタンスが作成されるたびに |
| ユースケース | 純粋な計算 | ファクトリ、デコレータ、コールバック |
7. init 関数とパッケージの初期化
(1) init関数の特徴
| 機能 | 説明 |
|---|---|
| 引数なし、戻り値なし | func init() |
| 自動呼び出し | パッケージがインポートされた際に自動的に実行される |
| 複数 | パッケージには複数の init 関数を定義できます(宣言された順に実行されます) |
| main の前 | main() の実行前に実行される |
▶ サンプル:パッケージの初期化(レジストリ)
// 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)
}
出力:
[init] registry initialized
[init] debug enabled
Registry: map[debug:1 max_connections:100 version:1]
(3) init と main の違い
| 関数 | 呼び出されるタイミング | 目的 |
|---|---|---|
init() |
パッケージのインポート時に自動的に実行される | グローバル変数を初期化し、ドライバを登録する |
main() |
プログラムの起動時に実行される(1回) | プログラムのエントリポイント |
8. 引数および戻り値としての関数(高階関数)
Go言語の関数は第一級オブジェクトであり、変数に代入したり、引数として渡したり、値として返したりすることができます。
(1) 引数としての関数(コールバック)
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)
}
出力:
doubled: [2 4 6 8 10]
squared: [1 4 9 16 25]
(2) 戻り値としての関数(ファクトリ)
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
}
出力:
15
105
▶ サンプル:関数型と高階関数(map/filter/reduce)
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)
}
出力:
doubled: [2 4 6 8 10 12 14 16 18 20]
evens: [2 4 6 8 10]
sum: 55
9. 完全な例:データ処理パイプライン
この関数のすべての機能を組み合わせて、ETL(抽出・変換・ロード)データパイプラインを構築します:
// 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)
}
期待される出力:
=== 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は関数レベルです。
❓ よくある質問
func foo(a ...int) または異なる関数名を使用してください。return ステートメントによりコード量が削減されます;(2) defer で戻り値を変更できます(重要!これはこのレッスンの総合的な例題で使用されています);(3) ドキュメント化――関数のシグネチャがドキュメントの役割を果たします。init 関数は手動で呼び出すことができますか?init は Go ランタイムによって自動的にのみ呼び出され、各パッケージの init は 1 回だけ実行されます。init を手動で呼び出そうとすると、コンパイルエラーが発生します。nums ...int は、本質的には関数内部の []int スライスですが、呼び出し規約が異なります:foo(1,2,3) 対 foo([]int{1,2,3}...)。前者は構文上の簡略化です。📖 まとめ
- 関数の定義は、4つの要素で構成されます:
func+ 名前 + パラメータリスト + 戻り値の型 - 複数の戻り値はGo言語の特徴であり、
(value, error)エラー処理パターンでよく用いられます - 戻り値に名前を付けることで、
returnステートメントがより簡潔になり、deferで戻り値を変更できるようになります - オーバーロードの代替手段としての可変引数
...;スライスは関数内で定義される - クロージャは外部変数をキャプチャするため、状態を持つ関数(カウンタやファクトリ関数など)の実装が可能になります
initは、パッケージがインポートされた際に自動的に実行され、グローバル状態の初期化に使用されます- 関数は第一級オブジェクトです。変数に代入したり、引数として渡したり、戻り値として返したりすることができ、高階関数(map/filter/reduce)もサポートしています。
📝 練習問題
-
基本問題(難易度 ⭐):すべての引数のうち最大の値を返す
max(nums ...int) int関数を記述してください。この関数は可変長引数を使用する必要があります。また、max(1, 5, 3, 9, 2)を呼び出した際に 9 が出力されるようにしてください。 -
応用問題(難易度 ⭐⭐):銀行口座をシミュレートする
makeBankAccount(initial int) (deposit func(int) int, withdraw func(int) (int, bool), balance func() int)を実装してください。depositは残高を増やし、新しい残高を返します。withdrawは資金を引き落とします(残高が不足している場合はfalseを返します)。balanceは残高を照会します。クロージャを使用する必要があります。 -
チャレンジ問題(難易度 ⭐⭐⭐):関数型データパイプラインを実装してください。入力
[]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。