Day 5: 関数 - 背景知識
目次
- 関数とは
- Go言語の関数の歴史と設計思想
- 基本的な関数
- 複数の戻り値
- 名前付き戻り値
- 可変長引数
- 無名関数とクロージャ
- 再帰関数
- 高階関数
- 実世界での関数の活用事例
- 関数のベストプラクティス
- パフォーマンスと最適化
---
関数とは
関数は、特定のタスクを実行するコードブロックをまとめたもので、プログラムの再利用性と保守性を高める最も基本的な抽象化の手段です。
func 関数名(引数) 戻り値の型 {
// 処理
return 値
}
関数の利点
---
Go言語の関数の歴史と設計思想
設計の背景
Go言語は2007年にGoogleでRobert Griesemer、Rob Pike、Ken Thompsonによって設計されました。関数の設計には以下の思想が反映されています:
1. シンプルさを追求
// Goの関数はシンプル
func add(a, b int) int {
return a + b
}
// Javaのような冗長さはない
// public static int add(int a, int b) { ... }
2. 複数戻り値によるエラーハンドリング
他の言語のtry-catch方式ではなく、明示的なエラー返却を採用:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
この設計により:
- エラーハンドリングが明示的
- 例外によるパフォーマンス低下がない
- 制御フローが追いやすい
3. 第一級関数(First-Class Functions)
関数を値として扱える設計は、関数型プログラミングの要素を取り入れています:
// 関数を変数に代入
f := func(x int) int {
return x * 2
}
// 関数を引数として渡す
func apply(fn func(int) int, value int) int {
return fn(value)
}
なぜこのような設計になったのか
---
基本的な関数
引数なし、戻り値なし
func sayHello() {
fmt.Println("Hello!")
}
func main() {
sayHello() // 関数呼び出し
}
使用シーン: 初期化処理、ログ出力、状態表示など
引数あり、戻り値なし
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
func printInfo(name string, age int, city string) {
fmt.Printf("%s is %d years old and lives in %s\n", name, age, city)
}
型の省略記法: 連続する同じ型の引数は省略可能
// aとbは両方int型
func add(a, b int) int {
return a + b
}
// 完全に書くと
func add(a int, b int) int {
return a + b
}
引数あり、戻り値あり
func add(a, b int) int {
return a + b
}
func multiply(x, y float64) float64 {
return x * y
}
func greeting(name string) string {
return "Hello, " + name + "!"
}
---
複数の戻り値
Goの最も特徴的な機能の一つが複数戻り値です。これはエラーハンドリングの基盤となっています。
基本的な複数戻り値
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
func main() {
q, r := divide(17, 5)
fmt.Printf("17 ÷ 5 = %d 余り %d\n", q, r) // 17 ÷ 5 = 3 余り 2
}
エラーハンドリングパターン
Go標準のエラーハンドリングパターン:
func openFile(filename string) (*os.File, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
return file, nil
}
// 使用例
func main() {
file, err := openFile("data.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// ファイル操作
}
複数の値と型
func getUserInfo(id int) (string, int, bool, error) {
// データベースから取得する想定
if id <= 0 {
return "", 0, false, errors.New("invalid user ID")
}
return "太郎", 25, true, nil // 名前、年齢、アクティブ、エラー
}
アンダースコアによる値の無視
不要な戻り値は_で無視できます:
// 商だけ必要で余りは不要
q, _ := divide(17, 5)
// エラーチェックをスキップ(非推奨)
file, _ := os.Open("data.txt") // エラーを無視するのは危険!
---
名前付き戻り値
名前付き戻り値は、戻り値に名前を付けることで可読性を向上させます。
基本的な使い方
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return // 名前付き戻り値は自動的に返される
}
メリット
func calculate(x, y int) (sum, diff, product int) {
sum = x + y
diff = x - y
product = x * y
return // sum, diff, productが返される
}
複雑な関数での活用
func processData(data []int) (min, max, sum int, err error) {
if len(data) == 0 {
err = errors.New("empty data")
return // min, max, sumは0、errはエラー
}
min = data[0]
max = data[0]
for _, v := range data {
sum += v
if v < min {
min = v
}
if v > max {
max = v
}
}
return // すべての名前付き戻り値が返される
}
注意点
名前付き戻り値を使用しても明示的に値を返すことができます:
func example() (result int) {
result = 10
return 20 // resultの値(10)ではなく20が返される
}
---
可変長引数
可変長引数を使うと、任意の数の引数を受け取れます。
基本的な可変長引数
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
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
}
スライスの展開
numbers := []int{1, 2, 3, 4, 5}
result := sum(numbers...) // スライスを展開して渡す
型の混在
可変長引数は最後のパラメータにのみ使用でき、他の引数と組み合わせられます:
func printf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}
printf("名前: %s, 年齢: %d\n", "太郎", 25)
実用例:ロガー
func log(level string, messages ...string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
fmt.Printf("[%s] %s: ", timestamp, level)
for i, msg := range messages {
if i > 0 {
fmt.Print(" | ")
}
fmt.Print(msg)
}
fmt.Println()
}
log("INFO", "Server started")
log("ERROR", "Database connection failed", "Retrying...", "Attempt 2")
---
無名関数とクロージャ
無名関数
名前を持たない関数を定義できます:
func main() {
// 無名関数の定義と即座の実行
func() {
fmt.Println("Hello from anonymous function")
}()
// 変数に代入
greet := func(name string) {
fmt.Printf("Hello, %s!\n", name)
}
greet("太郎")
}
クロージャ
外部スコープの変数を「キャプチャ」できます:
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
c1 := counter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
c2 := counter()
fmt.Println(c2()) // 1 (独立したカウンタ)
}
クロージャの実用例:設定の保存
func multiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func main() {
double := multiplier(2)
triple := multiplier(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15
}
フィルタ関数の実装
func filter(numbers []int, predicate func(int) bool) []int {
result := []int{}
for _, n := range numbers {
if predicate(n) {
result = append(result, n)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 偶数のみ
evens := filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4 6 8 10]
// 5より大きい数
greaterThan5 := filter(numbers, func(n int) bool {
return n > 5
})
fmt.Println(greaterThan5) // [6 7 8 9 10]
}
---
再帰関数
自分自身を呼び出す関数を再帰関数と呼びます。
基本的な再帰:階乗
func factorial(n int) int {
// 基底ケース(再帰の終了条件)
if n <= 1 {
return 1
}
// 再帰ケース
return n * factorial(n-1)
}
// 5! = 5 * 4 * 3 * 2 * 1 = 120
fmt.Println(factorial(5)) // 120
フィボナッチ数列
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
// フィボナッチ数列: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
for i := 0; i < 10; i++ {
fmt.Printf("%d ", fibonacci(i))
}
// 出力: 0 1 1 2 3 5 8 13 21 34
再帰の注意点:スタックオーバーフロー
// 危険な例:基底ケースがない
func infiniteRecursion(n int) int {
return infiniteRecursion(n-1) // 無限に続く
}
// runtime: goroutine stack exceeds 1000000000-byte limit
末尾再帰最適化の代替:ループ
再帰はエレガントですが、パフォーマンスの問題があります。Goは末尾再帰最適化を行わないため、ループで書き直すのが推奨される場合も:
// 再帰版(遅い)
func fibRecursive(n int) int {
if n <= 1 {
return n
}
return fibRecursive(n-1) + fibRecursive(n-2)
}
// 反復版(速い)
func fibIterative(n int) int {
if n <= 1 {
return n
}
a, b := 0, 1
for i := 2; i <= n; i++ {
a, b = b, a+b
}
return b
}
メモ化による最適化
func fibMemo(n int, memo map[int]int) int {
if n <= 1 {
return n
}
if val, ok := memo[n]; ok {
return val
}
memo[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo)
return memo[n]
}
func fibonacci(n int) int {
memo := make(map[int]int)
return fibMemo(n, memo)
}
---
高階関数
関数を引数として受け取る、または関数を戻り値として返す関数を高階関数と呼びます。
Map関数の実装
func mapInts(slice []int, fn func(int) int) []int {
result := make([]int, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
// 各要素を2倍
doubled := mapInts(numbers, func(n int) int {
return n * 2
})
fmt.Println(doubled) // [2 4 6 8 10]
}
Reduce関数の実装
func reduce(slice []int, initial int, fn func(int, int) int) int {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
// 合計
sum := reduce(numbers, 0, func(acc, n int) int {
return acc + n
})
fmt.Println(sum) // 15
// 積
product := reduce(numbers, 1, func(acc, n int) int {
return acc * n
})
fmt.Println(product) // 120
}
---
実世界での関数の活用事例
1. Google: 大規模システムでの関数設計
Googleでは、Goの関数を使って数千万行のコードベースを管理しています。
// Google Cloud Platformのエラーハンドリングパターン
func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) {
req := &pb.GetUserRequest{UserId: userID}
resp, err := c.client.GetUser(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to get user %s: %w", userID, err)
}
return convertUser(resp), nil
}
年収データ: Googleのシニアソフトウェアエンジニア(Go)は平均1,800万円〜3,000万円
2. Docker: コンテナ管理の関数パターン
// Dockerのコンテナライフサイクル管理
func (c *Container) Start(ctx context.Context) error {
if err := c.validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
if err := c.create(ctx); err != nil {
return err
}
return c.run(ctx)
}
市場価値: コンテナ技術+Goスキルは需要が高く、年収1,200万円〜2,000万円
3. Kubernetes: 関数型プログラミングパターン
// Kubernetesのリソース操作
func ApplyDefaults(pod *v1.Pod) {
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].ImagePullPolicy == "" {
pod.Spec.Containers[i].ImagePullPolicy = v1.PullIfNotPresent
}
}
}
キャリアパス: Kubernetesエンジニアとして年収1,500万円〜2,500万円
4. Uber: 高階関数によるミドルウェアパターン
// HTTPミドルウェアチェーン
type Middleware func(http.Handler) http.Handler
func Chain(middlewares ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
}
return final
}
}
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
5. Netflix: 関数を使ったエラーリトライ戦略
func RetryWithBackoff(fn func() error, maxRetries int) error {
backoff := time.Second
for i := 0; i < maxRetries; i++ {
if err := fn(); err != nil {
if i == maxRetries-1 {
return err
}
time.Sleep(backoff)
backoff *= 2
continue
}
return nil
}
return nil
}
---
関数のベストプラクティス
1. 単一責任の原則(Single Responsibility Principle)
関数は一つのことだけをすべきです:
// 悪い例:複数の責任
func processUserAndSendEmail(user User) error {
// ユーザーを検証
if user.Email == "" {
return errors.New("invalid email")
}
// データベースに保存
db.Save(user)
// メールを送信
sendEmail(user.Email, "Welcome!")
return nil
}
// 良い例:責任を分離
func validateUser(user User) error {
if user.Email == "" {
return errors.New("invalid email")
}
return nil
}
func saveUser(user User) error {
return db.Save(user)
}
func sendWelcomeEmail(email string) error {
return sendEmail(email, "Welcome!")
}
2. DRY原則(Don't Repeat Yourself)
// 悪い例:重複したコード
func calculateCircleArea(radius float64) float64 {
return 3.14159 * radius * radius
}
func calculateCircleCircumference(radius float64) float64 {
return 2 * 3.14159 * radius
}
// 良い例:定数を抽出
const Pi = 3.14159265359
func calculateCircleArea(radius float64) float64 {
return Pi * radius * radius
}
func calculateCircleCircumference(radius float64) float64 {
return 2 * Pi * radius
}
3. 早期リターン(Early Return)
// 悪い例:ネストが深い
func processOrder(order Order) error {
if order.Valid {
if order.Amount > 0 {
if order.CustomerID != "" {
// 処理
return nil
} else {
return errors.New("customer ID required")
}
} else {
return errors.New("invalid amount")
}
} else {
return errors.New("invalid order")
}
}
// 良い例:早期リターン
func processOrder(order Order) error {
if !order.Valid {
return errors.New("invalid order")
}
if order.Amount <= 0 {
return errors.New("invalid amount")
}
if order.CustomerID == "" {
return errors.New("customer ID required")
}
// 処理
return nil
}
4. 関数は短く保つ
経験則:関数は画面1ページに収まるべき(約50行以内)
5. 意味のある関数名
// 悪い例
func f(x int) int { return x * 2 }
func proc() { ... }
// 良い例
func double(number int) int { return number * 2 }
func processPayment() { ... }
---
パフォーマンスと最適化
インライン展開
小さな関数はコンパイラによってインライン展開されます:
// このような小さな関数は自動的にインライン化される
func add(a, b int) int {
return a + b
}
ベンチマーク
func BenchmarkFactorialRecursive(b *testing.B) {
for i := 0; i < b.N; i++ {
factorial(20)
}
}
func BenchmarkFactorialIterative(b *testing.B) {
for i := 0; i < b.N; i++ {
factorialIter(20)
}
}
メモリアロケーションの削減
// 悪い例:毎回新しいスライスを作成
func filter(numbers []int) []int {
result := []int{} // 容量が不明
for _, n := range numbers {
if n%2 == 0 {
result = append(result, n)
}
}
return result
}
// 良い例:事前に容量を確保
func filter(numbers []int) []int {
result := make([]int, 0, len(numbers)) // 最大容量を事前確保
for _, n := range numbers {
if n%2 == 0 {
result = append(result, n)
}
}
return result
}
---
開発手法とワークフロー
テスト駆動開発(TDD)
// 1. テストを先に書く
func TestAdd(t *testing.T) {
result := add(2, 3)
if result != 5 {
t.Errorf("add(2, 3) = %d; want 5", result)
}
}
// 2. 実装する
func add(a, b int) int {
return a + b
}
// 3. リファクタリング
func add(a, b int) int {
return a + b // すでにシンプルなのでそのまま
}
テーブル駆動テスト
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
wantQ int
wantR int
wantError bool
}{
{"normal", 10, 3, 3, 1, false},
{"exact", 10, 5, 2, 0, false},
{"divide by zero", 10, 0, 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, r, err := divide(tt.a, tt.b)
if (err != nil) != tt.wantError {
t.Errorf("divide() error = %v, wantError %v", err, tt.wantError)
}
if q != tt.wantQ || r != tt.wantR {
t.Errorf("divide() = (%d, %d), want (%d, %d)", q, r, tt.wantQ, tt.wantR)
}
})
}
}
---
よくある間違いと対策
1. 戻り値の無視
// 悪い例
file, _ := os.Open("data.txt") // エラーを無視
// 良い例
file, err := os.Open("data.txt")
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
2. nilチェック漏れ
// 悪い例
func getUser(id int) *User {
// データベース検索
return nil // 見つからない場合
}
user := getUser(123)
fmt.Println(user.Name) // パニック!
// 良い例
func getUser(id int) (*User, error) {
// データベース検索
if notFound {
return nil, errors.New("user not found")
}
return user, nil
}
3. クロージャのループ変数キャプチャ
// 悪い例
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // すべて3を出力
})
}
// 良い例
funcs := []func(){}
for i := 0; i < 3; i++ {
i := i // シャドウイング
funcs = append(funcs, func() {
fmt.Println(i) // 0, 1, 2を出力
})
}
---
まとめ
関数はGoプログラミングの基礎であり、以下の点を習得することが重要です:
次のステップ:
- Day 6でスライスと配列を学び、データ構造の操作を習得
- 実際のプロジェクトで関数を活用
- オープンソースのGoプロジェクトでコードリーディング
---
参考資料
公式ドキュメント
書籍
- "The Go Programming Language" by Alan A. A. Donovan and Brian W. Kernighan
- "Go in Action" by William Kennedy
オンラインリソース
- Go Playground - コードを試す
- Go Blog - 公式ブログ
- Awesome Go - Goのリソース集