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-catchGo だと:
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.log1. 基本: 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, errorGo コードを書くということは、ほぼ毎行
, 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点
- 定義側:
func sum(nums ...int) intで...int(型の前にドット3つ)- 呼び出し側:
- 値を直接:
sum(1, 2, 3)- スライスを展開:
sum(slice...)(末尾にドット3つ)JS で言うと: rest parameter
function sum(...nums: number[])とほぼ同じ。展開も spreadsum(...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 ではなく空スライス:
numsはnilでなく[]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 ← 独立している
}クロージャの本質
「外側のスコープにある変数を、関数の中に閉じ込めて持ち歩く」仕組み。関数を返す関数を書くと、内側の関数は外側の変数を覚えている。
makeCounterのcountは本来関数終了時に消えるはずだが、内側の無名関数が参照しているので死なずに残る。さらに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 との対比
JS Go 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) intとfunc(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. セッション録画を終了
exit2. 今日の発見(このノートに追記)
- 多値返却を書いてみた感想:
- クロージャ、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など)に抵抗がない
次のレッスン
Go のコレクション型の代表 スライス(可変長配列)と マップ(連想配列)を扱う。スライスは「内部構造」と「共有の落とし穴」が最大の関門。
つながりの予告
- 本章の高階関数
apply(slice, fn)は次章でfor ... range+ クロージャ に発展 - 多値返却の
(value, ok)パターンは次章 map の comma-ok idiom で再登場 - 可変長引数
...Tの構文はappend(slice, items...)で再活用