1-4. 関数 - 定義、多値返却、named return、可変長、クロージャ

所要時間: 30-50分(がっつりなら2セッション分) コミット内容: ~/learn/go/day04/funcs/ 一式


このレッスンのゴール

  • func f(a int) (int, error) の文法が手に馴染む
  • (result, err) 多値返却パターンを設計できる
  • named return + defer でエラーラップを書ける
  • 可変長引数 ...T の定義と展開呼び出し slice... ができる
  • クロージャと first-class function を実務シナリオで使える

なぜ学ぶか

TS では Result<T, E> で頑張ってきたが、Go ではどう書く?」 - 答えは「そのまま 2つの値を返す」というシンプルさ。Go の関数は エラーハンドリングの基盤 であり、ハンドラ・ミドルウェア・テストヘルパー など Level 2-3 で扱う全ての構造が「関数を返す関数」「関数を引数にする関数」で出来上がっている。本章を曖昧に流すと、Level 2 のミドルウェアで詰む。

前章とのつながり

1-3_制御構文n, err := strconv.Atoi(s)if err != nil で受け取った。本章ではそういう関数を 自分で書く側 に回る。「どうやってエラーを返す関数を設計するか」が中心。

これができると何が嬉しいか

  • 戻り値の意味が型から読める Go コードが書ける → 自己ドキュメント化
  • ミドルウェアパターン が書ける → Level 2 で楽になる
  • テーブル駆動テストのヘルパー関数 が書ける → Level 3 で楽になる

ストーリー導入: 多値返却は「レシートと結果を同時に渡す」

レストランで「料理 + レシート」が一緒に渡されるイメージ。料理(=result)だけ受け取ってレシート(=err)を捨てると、後で経費精算で詰む。Go の世界では「料理だけ寄こせ」と言う人(_ でエラーを捨てる人)は信用されない。result, err := ...; if err != nil { return err } が手癖になるまで が本章のゴール。


大前提: Go の関数は「多値返却」が当たり前

JS/TS の関数は基本「戻り値1つ」。複数返したい時はオブジェクトか配列に詰める。Go は違う:

result, err := strconv.Atoi("42")

戻り値を2つ並べて返せる。これが Go では当たり前の作法で、エラーハンドリングの基盤になっている。

JS だと:

// JS だと結果かエラーをオブジェクトで包む
const { result, error } = parseNum("42");
// もしくは throw / try-catch

Go だと:

result, err := parseNum("42")
if err != nil { ... }

**try/catch も無ければ Result 型もない。**ただ単に「値を2つ返す」。シンプル。


セッション①: 関数定義と多値返却(25-30分)

0. 録画スタート&作業ディレクトリ

mkdir -p ~/log ~/learn/go/day04/funcs
cd ~/learn/go/day04/funcs
go mod init example.com/funcs
script ~/log/go_day04.log

1. 基本: func 名前 (引数) 戻り値型

package main
 
import "fmt"
 
// 引数なし、戻り値なし
func hello() {
	fmt.Println("Hello!")
}
 
// 引数1つ、戻り値1つ
func square(x int) int {
	return x * x
}
 
// 引数2つ
func add(a int, b int) int {
	return a + b
}
 
// 同じ型の引数はまとめられる
func sub(a, b int) int {
	return a - b
}
 
func main() {
	hello()
	fmt.Println(square(4))
	fmt.Println(add(2, 3))
	fmt.Println(sub(10, 4))
}

関数定義の文法

func 名前(引数1 型1, 引数2 型2, ...) 戻り値型 {
	// 本体
	return 値
}

JS/TS との違いポイント

  • function ではなく func
  • 型は名前の後ろ(TS と同じ位置)
  • 戻り値の型は () の後ろに書く(TS の ): number { と同じ感覚)
  • 同じ型の連続引数はまとめられる: func add(a, b int)int, int
  • 可視性は名前の頭文字: 大文字始まりで外部公開、小文字始まりで非公開(前レッスン参照)

関数の実務ユースケース

  • 副作用なしのユーティリティ: 計算、フォーマット、変換
  • HTTP ハンドラ: func handleUsers(w http.ResponseWriter, r *http.Request)
  • テスト関数: func TestUserCreate(t *testing.T)(命名規則あり)

Goの関数は「クラスのメソッドより先に存在感がある」。いきなり関数を書いて、必要になったら構造体メソッドに昇格するスタイル。

2. 多値返却

// 商と余りを同時に返す
func divmod(a, b int) (int, int) {
	return a / b, a % b
}
 
func main() {
	q, r := divmod(17, 5)
	fmt.Println(q, r) // 3 2
}

多値返却の本質

戻り値の型を (int, int) のように 複数列挙するreturn a, b のようにカンマで並べて返す。受け取り側も q, r := ... で複数の変数に分解できる。

JS で言うと: 配列分割代入 const [q, r] = divmod(17, 5) に近い。ただし Go の方が型が個別に決まる(配列だと同じ型しか入らない)。

慣用句: result, err のペア

Go 標準ライブラリ・サードパーティの 9割以上の関数は最後の戻り値が error

n, err := strconv.Atoi("42")      // 値, error
f, err := os.Open("/etc/hosts")    // *os.File, error
data, err := io.ReadAll(f)         // []byte, error
resp, err := http.Get(url)         // *http.Response, error

Go コードを書くということは、ほぼ毎行 , err := を書くということ。これに慣れる。

_ で値を捨てる

不要な戻り値は _ (blank identifier) で受け取って捨てる:

// 商だけ欲しい
q, _ := divmod(17, 5)
 
// エラーだけ気になる
_, err := f.Write(data)
 
// 値だけ欲しい(エラー無視はアンチパターン)
n, _ := strconv.Atoi(s)  // 本番では NG、テスト・確実な入力のみ

使い所: 戻り値の一部だけ必要な時。

アンチパターン: _ でエラーを捨てる。「エラー無視」はGo の最大の事故源

多値返却の落とし穴

  • 受け取り変数の数を間違えるとコンパイルエラー: q := divmod(17, 5) は NG(2つ返るので2つ受ける必要がある)
  • エラーを _ で捨てるとレビューで指摘される: 「ここで失敗したら何が起きるの?」と必ず聞かれる
  • 戻り値2つを if 内で直接使えない: if x := divmod(17, 5) > 0 { } のような書き方は不可。一度変数に受ける

3. named return values(名前付き戻り値)

// 戻り値に名前を付ける
func divide(a, b int) (result int, err error) {
	if b == 0 {
		err = fmt.Errorf("0で割れません")
		return // 裸のreturn = 名前付き変数の現在値を返す
	}
	result = a / b
	return
}
 
func main() {
	r, err := divide(10, 2)
	fmt.Println(r, err) // 5 <nil>
 
	r, err = divide(10, 0)
	fmt.Println(r, err) // 0 0で割れません
}

named return values の本質

関数定義の戻り値型に 名前 を付けると、その名前が関数内の変数として使える。return だけ書けば、その時点での名前付き変数の値が返る(naked return)。

メリット

  • 戻り値の意味がドキュメントになる(result int, err error) を見れば返すものが分かる)
  • defer で戻り値を加工できる (後述)

デメリット

  • 長い関数で naked return を使うと読みづらい
  • 名前を付けたのに使わないと冗長

実務での使い分け

  • 短い関数で意味を伝えたい時 : 使う
  • defer で戻り値を加工したい時 : 必須レベル
  • 長い関数 : 普通の return value 形式の方が読みやすい

Go 標準ライブラリでも使う場面は限定的。**「使えるが多用しない」**スタンスでOK。

defer で名前付き戻り値を加工する典型例

func wrappedOperation() (err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("operation failed: %w", err)
		}
	}()
 
	if err = step1(); err != nil {
		return
	}
	if err = step2(); err != nil {
		return
	}
	return nil
}

err という名前を defer で書き換えて、エラーラップを統一的に行うパターン。中級者でよく見る。


セッション②: 可変長引数、クロージャ、first-class function(25-30分)

4. 可変長引数(variadic)

// ...int で「int を任意個」
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(1, 2, 3, 4, 5))     // 15
	fmt.Println(sum())                   // 0(空でもOK)
 
	// スライスから渡す時は ... を付ける
	nums := []int{10, 20, 30}
	fmt.Println(sum(nums...)) // 60
}

可変長引数 ...型 の本質

引数を「何個でも受け取れる」関数を作れる。関数の中では その型のスライスとして扱われる。

書き方の2点

  1. 定義側: func sum(nums ...int) int...int(型の前にドット3つ)
  2. 呼び出し側:
    • 値を直接: sum(1, 2, 3)
    • スライスを展開: sum(slice...) (末尾にドット3つ)

JS で言うと: rest parameter function sum(...nums: number[]) とほぼ同じ。展開も spread sum(...nums) で同じ作法。

可変長引数の実務ユースケース

  • fmt.Println(a, b, c) : Println...interface{} で何でも受け取る
  • fmt.Printf(format, args...) : スライスを展開して渡す典型例
  • strings.Join(parts, sep) とは違って、自前の連結関数を作る時
  • ログ関数のラッパー: func log(msg string, args ...interface{})

可変長引数の落とし穴

  • 可変長引数は 関数の最後の引数 にしか書けない: func f(a int, b ...int, c string) は NG
  • スライスを直接渡しても展開されない: sum(slice) だと sum([]int) という別シグネチャの呼び出しと見なされる。必ず ... を付ける
  • 空の可変長は nil ではなく空スライス: numsnil でなく []int{} として扱える(が、len(nums) == 0 で判定するのが安全)

5. クロージャ - 関数が外側の変数を覚える

package main
 
import "fmt"
 
// カウンタを作る関数
func makeCounter() func() int {
	count := 0
	return func() int {
		count++
		return count
	}
}
 
func main() {
	counter1 := makeCounter()
	counter2 := makeCounter()
 
	fmt.Println(counter1()) // 1
	fmt.Println(counter1()) // 2
	fmt.Println(counter1()) // 3
	fmt.Println(counter2()) // 1 ← 独立している
}

クロージャの本質

「外側のスコープにある変数を、関数の中に閉じ込めて持ち歩く」仕組み。関数を返す関数を書くと、内側の関数は外側の変数を覚えている。

makeCountercount は本来関数終了時に消えるはずだが、内側の無名関数が参照しているので死なずに残る。さらに makeCounter() を2回呼ぶと 別々の count が作られる

JS のクロージャと完全に同じ概念。JSで触れたことがあるなら新鮮さは無いはず。

クロージャの実務ユースケース

  • ステートを持ったハンドラの生成: 「DB接続を握ったハンドラ関数を作る」関数
  • 設定済みロガーの生成: prefix を持った log 関数を返す
  • コールバック / イテレータの一時関数
  • defer func() { ... }() で関数内変数をキャプチャ: defer の常套句
func newLogger(prefix string) func(string) {
	return func(msg string) {
		fmt.Printf("[%s] %s\n", prefix, msg)
	}
}
 
info := newLogger("INFO")
err := newLogger("ERROR")
info("started")  // [INFO] started
err("crashed")   // [ERROR] crashed

クロージャの落とし穴: ループ変数のキャプチャ

// Go 1.21 以前: 全部「3」になる罠(有名な罠)
funcs := []func(){}
for i := 0; i < 3; i++ {
	funcs = append(funcs, func() { fmt.Println(i) })
}
for _, f := range funcs {
	f() // 1.21以前: 3 3 3、1.22以降: 0 1 2
}

Go 1.22 で「ループ変数は毎回新しく」に仕様変更されたので、現代のGo環境では悩むことが減った。

古いコードを読む時は注意。回避策は明示的にコピー:

for i := 0; i < 3; i++ {
	i := i // ← 同名で新しい変数を作る
	funcs = append(funcs, func() { fmt.Println(i) })
}

6. 関数を値として扱う(first-class function)

package main
 
import "fmt"
 
// 関数を引数として受け取る
func apply(nums []int, fn func(int) int) []int {
	result := make([]int, len(nums))
	for i, n := range nums {
		result[i] = fn(n)
	}
	return result
}
 
// 関数を値として渡す
func double(x int) int { return x * 2 }
func square(x int) int { return x * x }
 
func main() {
	nums := []int{1, 2, 3, 4, 5}
 
	// 名前付き関数を渡す
	fmt.Println(apply(nums, double)) // [2 4 6 8 10]
 
	// 無名関数(ラムダ)を渡す
	fmt.Println(apply(nums, func(x int) int { return x + 100 }))
	// [101 102 103 104 105]
 
	// 関数を変数に
	fn := square
	fmt.Println(fn(7)) // 49
}

first-class function の本質

関数を 値(int や string と同じ扱い)として変数に入れたり引数で渡したり戻り値で返したり できる性質。Go の関数はすべて first-class。

JS との対比

JSGo
const f = x => x * 2f := func(x int) int { return x * 2 }
[1,2,3].map(f)apply(nums, f)(mapは自作)
function fn(cb) { cb(); }func fn(cb func()) { cb() }

「JS の関数渡しと同じ感覚」で書ける。型を明示する分だけ冗長。

関数型の書き方

  • 無引数・戻り値なし: func()
  • 引数あり: func(int), func(int, string)
  • 戻り値あり: func(int) int, func() (string, error)
  • 両方あり: func(int, int) (int, error)

慣れないうちは混乱しがちなので、エディタの補完を頼る。

関数型の落とし穴

  • 関数のシグネチャがちょっとでも違うと型が違う: func(int) intfunc(int) (int, error) は別物
  • 関数の比較はできないf1 == f2 はコンパイルエラー、nil との比較のみ可)
  • メソッド値とただの関数値は別(中級者の罠。最初は気にしなくていい)

高階関数で「処理を後から差し替える」

// どう変換するかを呼び出し側に任せる
func transform(items []string, fn func(string) string) []string {
	result := make([]string, len(items))
	for i, s := range items {
		result[i] = fn(s)
	}
	return result
}
 
import "strings"
 
names := []string{"alice", "bob", "carol"}
 
upper := transform(names, strings.ToUpper)
fmt.Println(upper) // [ALICE BOB CAROL]
 
exclaim := transform(names, func(s string) string {
	return s + "!"
})
fmt.Println(exclaim) // [alice! bob! carol!]

練習課題

// main.go を以下で書き換えて動かす
 
package main
 
import (
	"fmt"
	"strings"
)
 
// 多値返却の練習
func divmod(a, b int) (int, int, error) {
	if b == 0 {
		return 0, 0, fmt.Errorf("0で割れません")
	}
	return a / b, a % b, nil
}
 
// 可変長引数の練習
func joinWith(sep string, parts ...string) string {
	return strings.Join(parts, sep)
}
 
// クロージャの練習 - 累積加算器
func makeAdder() func(int) int {
	total := 0
	return func(x int) int {
		total += x
		return total
	}
}
 
// 高階関数 - フィルター
func filter(nums []int, pred func(int) bool) []int {
	result := []int{}
	for _, n := range nums {
		if pred(n) {
			result = append(result, n)
		}
	}
	return result
}
 
func main() {
	// 1. 多値返却 + エラー
	q, r, err := divmod(17, 5)
	if err != nil {
		fmt.Println("エラー:", err)
	} else {
		fmt.Printf("17 / 5 = %d 余り %d\n", q, r)
	}
 
	// 2. 可変長
	fmt.Println(joinWith("-", "2026", "05", "14")) // 2026-05-14
 
	// 3. クロージャ
	add := makeAdder()
	fmt.Println(add(10)) // 10
	fmt.Println(add(20)) // 30
	fmt.Println(add(5))  // 35
 
	// 4. 高階関数で偶数だけ抽出
	nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	evens := filter(nums, func(n int) bool {
		return n%2 == 0
	})
	fmt.Println(evens) // [2 4 6 8 10]
}
go run .

動作確認のポイント

  • divmod(10, 0) でエラーが返ってくる
  • makeAdder を2つ作って独立性を確認してもいい
  • filter の述語を n > 5 などに変えて遊ぶ

締め: 振り返り(10分)

1. セッション録画を終了

exit

2. 今日の発見(このノートに追記)

- 多値返却を書いてみた感想:
- クロージャ、JS と比べてどう?:
- named return を使ってみた / 使わなかった理由:
- 明日やりたいこと:

アンチパターン集 - やらかし事例

関数設計のやらかし

1. エラーを最後ではなく最初に書く

func f() (error, int) { ... }  // ← Go 流に反する

Go では error は最後の戻り値 が絶対のルール。これを破ると標準ライブラリと噛み合わず、if err != nil パターンが書きづらい。

2. 名前付き戻り値の濫用

func longFunction() (result int, count int, status string, err error) {
    // 100行...
    return  // naked return - 何返してるか分からない
}

名前付き戻り値は 短い関数 or defer でエラーラップ の時だけ。長い関数では普通の return value で明示。

3. クロージャでループ変数を捕捉して全部同じ値

// Go 1.21 以前
for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()  // 全部 3 になる
}

Go 1.22 で修正されたが、古いコードベースを触る時は i := i のシャドーイングで明示的にコピー。

4. 可変長引数のスライスをそのまま渡し忘れ

nums := []int{1, 2, 3}
sum(nums)     // NG: 型エラー([]int を ...int に渡せない)
sum(nums...)  // OK

対比表で違いを明確化

値レシーバ vs 関数 vs クロージャ

観点普通の関数メソッドクロージャ
状態引数のみレシーバ + 引数外側の変数を保持
書き方func f(x int)func (u *User) f(x int)func() func(int){...}
用途純粋計算データ + 振る舞いステート付き処理

関数型シグネチャの読み方

シグネチャ意味
func()引数なし、戻り値なし
func(int) intint 受け取り int 返す
func() (string, error)引数なし、文字列とエラー返す
func(http.ResponseWriter, *http.Request)HTTP ハンドラの型
func(next http.Handler) http.Handlerミドルウェアの型

自己評価チェックリスト

手を動かせた

  • func 名前(引数 型) 戻り値型 { } を書いた
  • 多値返却 (int, error) を返す関数を書いた
  • _ で戻り値を捨てた(テスト時のみ)
  • 可変長引数 ...int を定義し slice... で展開した
  • クロージャでカウンタを作った
  • 関数を引数として渡した(高階関数)

説明できる

  • エラーが最後の戻り値である理由
  • named return が役立つ場面
  • クロージャが「外側の変数を持ち回る」仕組み
  • first-class function の意味

やらかし回避

  • エラーを _ で捨てるアンチパターンを認識
  • ループ変数キャプチャの罠(古い Go)
  • 名前付き戻り値の濫用を避ける

詰まった時のチートシート

やりたいこと書き方
関数定義func name(a int) int { return a }
同型引数まとめるfunc add(a, b int)
戻り値2つfunc f() (int, error) { return 0, nil }
戻り値受け取りv, err := f()
戻り値捨てる_, err := f()
名前付き戻り値func f() (n int, err error) { return }
可変長引数func sum(nums ...int) int
可変長を渡すsum(slice...)
無名関数func(x int) int { return x*2 }
関数を変数にf := func() { ... }
関数を引数にfunc apply(fn func(int) int)
クロージャ外側の変数を内側の関数で参照

「実務OK」基準

  • result, err := f(); if err != nil { return err } が手癖になっている
  • 多値返却で困らない: 戻り値の数を間違えない
  • ...args を見た時に「可変長か」と即理解できる
  • クロージャを「state を持ち回る関数」として認識できる
  • 関数を引数に取る API(sort.Slice など)に抵抗がない

次のレッスン

1-5. スライスとマップ へ。

Go のコレクション型の代表 スライス(可変長配列)と マップ(連想配列)を扱う。スライスは「内部構造」と「共有の落とし穴」が最大の関門。

つながりの予告

  • 本章の高階関数 apply(slice, fn) は次章で for ... range + クロージャ に発展
  • 多値返却の (value, ok) パターンは次章 map の comma-ok idiom で再登場
  • 可変長引数 ...T の構文は append(slice, items...) で再活用