コンテンツにスキップ

Go/関数

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go


関数の基礎

[編集]

関数とは

[編集]

関数は、特定の処理をまとめた再利用可能なコードブロックです。Goの関数は、プログラムを構造化し、コードの重複を避け、保守性を高めるための重要な要素です。適切に設計された関数は、読みやすく、テストしやすく、再利用性の高いコードを実現します。

関数の定義と呼び出し

[編集]

Goでは、funcキーワードを使用して関数を定義します。基本的な構文は以下の通りです:

package main

import "fmt"

func greeting(name string) string {
    return "Hello, " + name + "!"
}

func main() {
    // 関数の呼び出し
    message := greeting("Gopher")
    fmt.Println(message) // 出力: Hello, Gopher!
}

この例では、greeting関数が定義され、main関数内から呼び出されています。greeting関数は文字列型のパラメータnameを受け取り、挨拶のメッセージを文字列として返します。

パラメータと引数

[編集]

Goでは、パラメータ(仮引数)と引数(実引数)を区別することが重要です:

  • パラメータ(仮引数):関数定義時の変数
  • 引数(実引数):関数呼び出し時に渡す値
// x, yはパラメータ
func add(x int, y int) int {
    return x + y
}

sum := add(5, 3) // 5, 3は引数

Goでは、同じ型のパラメータが連続する場合、型を一度だけ記述することができます:

func add(x, y int) int {
    return x + y
}

この短縮形は読みやすさを向上させ、コードの冗長性を減らします。

戻り値の基本

[編集]

関数は処理結果を戻り値として返すことができます。Goでは関数の戻り値の型を明示的に指定する必要があります。

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

この例では、square関数は整数値を受け取り、その二乗を計算して整数値として返します。

複数の引数と戻り値

[編集]

複数の引数を持つ関数

[編集]

Goの関数は複数の引数を受け取ることができます。これにより、様々な入力値に基づいて処理を行うことが可能になります。

func calculate(x int, y int, operator string) int {
    switch operator {
    case "+":
        return x + y
    case "-":
        return x - y
    case "*":
        return x * y
    case "/":
        if y != 0 {
            return x / y
        }
        return 0
    default:
        return 0
    }
}

この関数は、2つの整数値と演算子を受け取り、指定された演算を実行します。

複数の戻り値を返す関数

[編集]

Goの特徴的な機能の一つに、複数の値を同時に返す能力があります。これは特にエラー処理に役立ちます。

package main

import "fmt"

// divide 関数の実装
func divide(x, y int) (int, error) {
	switch {
	case x == 0 && y == 0:
		return 0, fmt.Errorf("division zero by zero")
	case y == 0:
		return 0, fmt.Errorf("division by zero")
	default:
		return x / y, nil
	}
}

func main() {
	testCases := [...][2]int{
		{10, 2},         // 正常な除算
		{10, 0},         // 除数がゼロ
		{0, 0},          // 0 ÷ 0 のケース
		{-10, 2},        // 負の数の除算
		{1000000000, 2}, // 大きな数での除算
	}

	for _, pair := range testCases {
		fmt.Print(pair[0], " / ", pair[1])
		if result, err := divide(pair[0], pair[1]); err != nil {
			fmt.Println("\t=> Error:", err)
		} else {
			fmt.Println("\t= ", result)
		}
	}
}

実行結果:

10 / 2   =  5
10 / 0   => Error: division by zero
0 / 0    => Error: division zero by zero
-10 / 2  =  -5 
1000000000 / 2    =  500000000

この例では、divide関数が計算結果とエラー情報を同時に返します。これはGoのイディオムの一つで、エラー処理を明示的かつ効率的に行うことができます。

名前付き戻り値

[編集]

Goでは戻り値に名前をつけることができます。これにより、関数内で戻り値の変数がゼロ値で初期化され、空のreturn文(blank return)を使用できます。

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // 空のreturn文(blank return)
}

名前付き戻り値は以下の場合に特に有用です:

  • 戻り値の意味が明確になる
  • 複数の戻り場所がある関数で、戻り値の一貫性を保つ
  • デファーされた関数で戻り値を操作する

以下は名前付き戻り値を使用した除算関数の実装例です:

package main

import "fmt"

// divide は整数の除算を行う関数。
// 引数:
// - x: 被除数
// - y: 除数
//
// 戻り値:
// - quotient: 除算の結果 (エラーがない場合のみ有効)
// - err: 除算が無効な場合に返されるエラー
//
// エラー条件:
// - x と y の両方が 0 の場合、"division zero by zero" を返す。
// - y が 0 の場合、"division by zero" を返す。
func divide(x, y int) (quotient int, err error) {
    switch {
    case x == 0 && y == 0:
        err = fmt.Errorf("division zero by zero")
    case y == 0:
        err = fmt.Errorf("division by zero")
    default:
        quotient = x / y
    }
    return
}

func main() {
    // テストケースの配列
    testCases := [5][2]int{
        {10, 2},         // 正常な除算
        {10, 0},         // 除数がゼロ
        {0, 0},          // 0 ÷ 0 のケース
        {-10, 2},        // 負の数の除算
        {1000000000, 2}, // 大きな数での除算
    }

    // テストケースをループで処理
    for _, pair := range testCases {
        fmt.Print(pair[0], " / ", pair[1])
        if result, err := divide(pair[0], pair[1]); err != nil {
            fmt.Println("\t=> エラー:", err)
        } else {
            fmt.Println("\t= ", result)
        }
    }
}
解説
  • divide 関数について:
    • この関数は、整数の除算を安全に行います。
    • xy の値をチェックし、不正な除算が試みられた場合はエラーを返します。
  • エラー処理:
    • xy が両方 0 の場合 (0 ÷ 0): division zero by zero エラー。
    • y が 0 の場合: division by zero エラー。
  • テストケース: 配列 testCases に以下のケースを用意しました:
    • 通常の除算
    • 除数が 0 の場合
    • 0 ÷ 0 の特殊ケース
    • 負の数での除算
    • 大きな数の除算
  • 出力の仕組み:
    • divide 関数を呼び出し、エラーがあればそれを表示。
    • エラーがなければ計算結果を表示。

関数の型

[編集]

関数の型宣言

[編集]

Goでは、関数も型として扱うことができます。これにより、関数型の変数を宣言したり、関数を他の関数に渡したりすることが可能になります。

package main

import "fmt"

// Operation は整数2つを受け取り整数を返す関数の型
type Operation func(x, y int) int

func add(x, y int) int {
    return x + y
}

func multiply(x, y int) int {
    return x * y
}

func main() {
    var op Operation = add
    fmt.Println(op(5, 3)) // 出力: 8

    op = multiply
    fmt.Println(op(5, 3)) // 出力: 15
}

この例では、Operationという関数型を定義し、その型の変数opに異なる関数を代入しています。

関数を変数に代入

[編集]

関数は変数に代入して使用することができます。特に匿名関数(無名関数)を変数に代入する場合に便利です。

package main

import "fmt"

func main() {
    // 匿名関数を変数に代入
    square := func(n int) int {
        return n * n
    }

    fmt.Println(square(5)) // 出力: 25
    
    // 別の計算関数を定義
    cube := func(n int) int {
        return n * n * n
    }
    
    fmt.Println(cube(3)) // 出力: 27
}

このアプローチにより、特定のスコープでのみ使用する小さな関数を効率的に定義できます。

関数を引数として渡す

[編集]

関数を他の関数の引数として渡すことができます。これは高階関数(higher-order functions)を作成する際に役立ちます。

package main

import "fmt"

func applyOperation(x, y int, operation func(int, int) int) int {
    return operation(x, y)
}

func main() {
    // 無名関数を引数として渡す
    sum := applyOperation(5, 3, func(x, y int) int {
        return x + y
    })
    fmt.Println("Sum:", sum) // 出力: Sum: 8
    
    // 乗算の例
    product := applyOperation(5, 3, func(x, y int) int {
        return x * y
    })
    fmt.Println("Product:", product) // 出力: Product: 15
}

この例では、applyOperation関数が整数と演算関数を受け取り、その関数を適用して結果を返します。

クロージャ

[編集]

クロージャとは

[編集]

クロージャ(closure)は、自身が定義されたスコープの変数を参照できる関数です。これにより、関数が呼び出されるたびに状態を保持することができます。

package main

import "fmt"

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    c := counter()
    fmt.Println(c()) // 出力: 1
    fmt.Println(c()) // 出力: 2
    fmt.Println(c()) // 出力: 3
    
    // 新しいカウンターインスタンス
    c2 := counter()
    fmt.Println(c2()) // 出力: 1(c2は独立した状態を持つ)
}

この例では、counter関数がクロージャを返します。このクロージャはcount変数にアクセスでき、呼び出しごとにその値を増やします。複数のカウンターインスタンスを作成すると、それぞれが独立したcount変数を持ちます。

クロージャの実践的な使い方

[編集]

クロージャは、状態を保持するだけでなく、複雑なロジックをカプセル化するためにも使用できます。フィボナッチ数列の生成は良い例です:

package main

import "fmt"

func fibonacci() func() int {
    prev, curr := 0, 1
    return func() int {
        result := prev
        prev, curr = curr, prev+curr
        return result
    }
}

func main() {
    fib := fibonacci()
    
    // 最初の10個のフィボナッチ数を表示
    for i := 0; i < 10; i++ {
        fmt.Println(fib())
    }
    // 出力: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

このクロージャは、フィボナッチ数列の次の値を計算するために必要な前の2つの値(prevcurr)を保持します。

可変長引数

[編集]

可変長引数の基本

[編集]

可変長引数(variadic parameters)を使用すると、任意の数の引数を関数に渡すことができます。これは、引数の数が可変である場合に便利です。

package main

import "fmt"

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

func main() {
    fmt.Println(sum(1, 2, 3, 4, 5)) // 出力: 15
    fmt.Println(sum(10, 20))        // 出力: 30
    fmt.Println(sum())              // 出力: 0(引数なし)
}

可変長引数は、関数内では特定の型のスライスとして扱われます。

スライスの展開

[編集]

既存のスライスを可変長引数として渡す場合は、...演算子を使用してスライスを展開します。

package main

import "fmt"

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

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(sum(numbers...)) // 出力: 15
    
    // 追加の引数と組み合わせることも可能
    moreNumbers := []int{6, 7, 8}
    fmt.Println(sum(append(numbers, moreNumbers...)...)) // 出力: 36
}

ここで重要なのは、スライスの後に...を付けることで、そのスライスの要素が個別の引数として展開されることです。

可変長引数の注意点

[編集]

可変長引数の挙動と副作用

[編集]

Goの可変長引数の挙動には注意が必要です。特に、スライスのポインタ渡し値渡しの違いが混乱を招くことがあります。

スライスのポインタ渡し
可変長引数は実際にはスライスとして扱われるため、関数内でスライスの内容を変更すると、呼び出し元のスライスも影響を受けます。
package main

import "fmt"

func modify(nums ...int) {
    nums[0] = 100
    fmt.Println("Inside modify:", nums) // Inside modify: [100 2 3 4 5]
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println("Before:", numbers) // Before: [1 2 3 4 5]

    modify(numbers...)              // 可変長引数を渡す
    fmt.Println("After:", numbers)  // After: [100 2 3 4 5]
    // 呼び出し元のスライスも変更されている
}

この例では、modify関数内でnumsの最初の要素を変更すると、呼び出し元のnumbersスライスもその変更の影響を受けます。

ただし、appendなどを使用して新しいスライスを作成する場合は、元のスライスには影響しません:

package main

import "fmt"

func modify(nums ...int) {
    nums = append(nums, 10)              // 新しいスライスを作成
    fmt.Println("Inside modify:", nums)  // Inside modify: [1 2 3 4 5 10]
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println("Before:", numbers)  // Before: [1 2 3 4 5]

    modify(numbers...)               // 可変長引数を渡す
    fmt.Println("After:", numbers)   // After: [1 2 3 4 5]
    // 呼び出し元のスライスは変更されていない
}

地雷となりやすいケース

[編集]
  • 意図しない副作用: 関数内でスライスを変更すると、呼び出し元に影響を与えることがあります。これは非常に注意が必要です。
  • 長い引数リスト: 大量のデータを可変長引数として渡す場合、メモリのコピーや効率性に関して問題が発生することもあります。
  • 関数内でスライスを再代入する場合: appendや新しいスライスの作成が関数内で行われると、呼び出し元のスライスは変更されません。この動作が予期せぬバグにつながることがあります。

対策

[編集]
  • 関数内でのスライスの変更を避ける: 変更が必要な場合は、その影響を理解し、必要に応じてコピーを使用する。
  • スライスを明示的にコピーする: 呼び出し元のデータを変更したくない場合は、スライスを関数内でコピーして使用する。
  • 引数として渡す前に十分な検討: 大きなデータを扱う場合は、可変長引数の使用を慎重に検討する。
package main

import "fmt"

// 安全な操作を行う関数
func safeModify(nums ...int) []int {
    // 入力スライスのコピーを作成
    result := make([]int, len(nums))
    copy(result, nums)
    
    // コピーを変更
    if len(result) > 0 {
        result[0] = 100
    }
    
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println("Before:", numbers)  // Before: [1 2 3 4 5]
    
    // 変更されたスライスを取得
    modified := safeModify(numbers...)
    fmt.Println("Original:", numbers) // Original: [1 2 3 4 5]
    fmt.Println("Modified:", modified) // Modified: [100 2 3 4 5]
}

この例では、safeModify関数が入力スライスのコピーを作成して変更し、変更されたスライスを戻り値として返します。このパターンを使うと副作用を避けることができます。

再帰的関数呼び出し

[編集]

Goにおける関数の再帰的呼び出しの例として、階乗の計算を考えてみましょう。階乗は数学的な概念で、正の整数 の階乗 () は、 から までのすべての正の整数を掛け合わせたものです。

ただし、 と定義されます。

Goでの再帰関数の実装例

[編集]
package main

import "fmt"

func factorial(n int) int {
	// ベースケース: 再帰呼び出しを終了させる条件
	// nが0の場合、1を返す
	if n == 0 {
		return 1
	}

	// 再帰ステップ: 関数自身を呼び出す
	// n * (n-1)! を計算するために、factorial(n-1) を呼び出す
	return n * factorial(n-1)
}

func main() {
	number := 5
	result := factorial(number)
	fmt.Printf("%dの階乗は%dです。\n", number, result) // 出力: 5の階乗は120です。

	number = 0
	result = factorial(number)
	fmt.Printf("%dの階乗は%dです。\n", number, result) // 出力: 0の階乗は1です。

	number = 3
	result = factorial(number)
	fmt.Printf("%dの階乗は%dです。\n", number, result) // 出力: 3の階乗は6です。
}

コードの説明

[編集]
  1. factorial 関数:
    • func factorial(n int) int は、整数 n を引数に取り、整数を返す関数を定義しています。
  2. ベースケース (Base Case):
    • if n == 0 { return 1 } の部分がベースケースです。再帰呼び出しが無限ループに陥らないようにするための終了条件となります。階乗の定義により、 なので、n になったら を返して処理を終了します。ベースケースがないと、関数は自分自身を無限に呼び出し続け、最終的にスタックオーバーフローを起こします。
  3. 再帰ステップ (Recursive Step):
    • return n * factorial(n-1) の部分が再帰ステップです。ここでは、factorial 関数が自分自身を引数 n-1 で呼び出しています。
    • 例えば factorial(5) が呼び出されると、以下のように処理が進みます。
      • 5 * factorial(4)
      • 5 * (4 * factorial(3))
      • 5 * (4 * (3 * factorial(2)))
      • 5 * (4 * (3 * (2 * factorial(1))))
      • 5 * (4 * (3 * (2 * (1 * factorial(0)))))
      • factorial(0) はベースケースに到達し 1 を返します。
      • 5 * (4 * (3 * (2 * (1 * 1))))
      • 5 * (4 * (3 * (2 * 1)))
      • 5 * (4 * (3 * 2))
      • 5 * (4 * 6)
      • 5 * 24
      • 120

再帰関数の利点と注意点

[編集]
利点
  • 一部の問題(特に木の探索や階乗のような数学的定義)では、コードがより直感的で簡潔になることがあります。
  • 問題の自然な構造を反映しやすいです。
注意点
  • ベースケースの定義が必須: 終了条件がないと無限ループになり、スタックオーバーフローが発生します。
  • パフォーマンス: 各再帰呼び出しは、スタックフレームの作成などのオーバーヘッドを伴うため、繰り返しの処理が多い場合は、ループ(反復)を使った実装の方が効率的な場合が多いです。
  • 可読性: 深い再帰呼び出しは、処理の流れを追うのが難しくなることがあります。

再帰関数は、適切に使えば非常に強力なツールですが、その特性を理解して慎重に適用することが重要です。

メソッド

[編集]

メソッドの定義

[編集]

Goにおけるメソッドは、特定の型に関連付けられた関数です。メソッドは、その型のインスタンスに対して操作を行うための手段を提供します。

package main

import "fmt"

type Rectangle struct {
    width, height float64
}

// Rectangleに対するメソッド
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

func main() {
    r := Rectangle{width: 10, height: 5}
    fmt.Println("面積:", r.Area()) // 出力: 面積: 50
}

この例では、AreaメソッドがRectangle型に関連付けられています。メソッドの定義では、(r Rectangle)というレシーバを指定しています。

ポインタレシーバとバリューレシーバ

[編集]

メソッドには、ポインタレシーババリューレシーバの2種類があります。

  • バリューレシーバ:レシーバの値のコピーを使用(変更は元のインスタンスに影響しない)
  • ポインタレシーバ:レシーバのポインタを使用(変更は元のインスタンスに影響する)
package main

import "fmt"

type Rectangle struct {
    width, height float64
}

// バリューレシーバを使用したメソッド
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

// ポインタレシーバを使用したメソッド
func (r *Rectangle) Scale(factor float64) {
    r.width *= factor
    r.height *= factor
}

func main() {
    r := Rectangle{width: 10, height: 5}
    
    fmt.Println("変更前の面積:", r.Area()) // 出力: 変更前の面積: 50
    
    r.Scale(2) // ポインタレシーバを使用して変更
    
    fmt.Println("変更後の面積:", r.Area()) // 出力: 変更後の面積: 200
    fmt.Printf("寸法: 幅 %.1f x 高さ %.1f\n", r.width, r.height) // 出力: 寸法: 幅 20.0 x 高さ 10.0
}

この例では、Scaleメソッドがポインタレシーバを使用してRectangleのサイズを変更します。これにより、元の構造体が直接変更されます。一方、Areaメソッドはバリューレシーバを使用し、構造体の状態を変更しません。

ポインタレシーバを使用すべき場合:

  • メソッドがレシーバを変更する必要がある場合
  • レシーバが大きなサイズの場合(コピーのコストを避ける)
  • 一貫性のため(型の他のメソッドがポインタレシーバを使用している場合)

defer文

[編集]

defer文の基本

[編集]

defer文は、関数の終了時に実行される処理を登録するための機能です。主に、リソースの解放やクリーンアップ処理に使用されます。

package main

import (
    "fmt"
    "os"
)

func processFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close() // 関数終了時に確実にファイルを閉じる
    
    // ファイル処理...
    fmt.Println("ファイル処理中...")
    
    return nil // 関数が終了する前にdeferで登録された処理が実行される
}

この例では、ファイルを開いた後にdefer f.Close()を呼び出しています。これにより、関数がどのように終了するかに関わらず(正常終了、エラー、panic)、ファイルは確実に閉じられます。

複数のdefer文の実行順序

[編集]

複数のdefer文が登録されている場合、それらはLIFO(Last In, First Out、後入れ先出し)の順序で実行されます。つまり、最後に登録されたdefer文が最初に実行されます。

package main

import "fmt"

func main() {
    fmt.Println("開始")
    
    defer fmt.Println("1番目のdefer")
    defer fmt.Println("2番目のdefer")
    defer fmt.Println("3番目のdefer")
    
    fmt.Println("処理中...")
    
    // 終了前に実行される(逆順)
}

実行結果:

開始
処理中...
3番目のdefer
2番目のdefer
1番目のdefer

この特性は、リソースの解放において重要です。通常、リソースは開いた順序と逆の順序で閉じるべきだからです。

defer文の実践的な使用例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func criticalSection(mutex *sync.Mutex, id int) {
	fmt.Printf("ゴルーチン %d: ロック取得中...\n", id)
	mutex.Lock()
	defer mutex.Unlock() // 確実にアンロックする

	fmt.Printf("ゴルーチン %d: 処理実行中\n", id)
	time.Sleep(100 * time.Millisecond) // 何らかの処理

	// エラーが発生しても、panicが起きても、defer文によりアンロックされる
	fmt.Printf("ゴルーチン %d: 処理完了\n", id)
}

func main() {
	mutex := sync.Mutex{}

	for i := range 3 {
		go criticalSection(&mutex, i)
	}

	// メインゴルーチンが終了しないように待機
	time.Sleep(500 * time.Millisecond)
}

この例では、defer文を使用してミューテックスのアンロックを保証しています。これにより、デッドロックを防ぐことができます。