コンテンツにスキップ

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!
}

パラメータと引数

[編集]

パラメータ(仮引数)は関数定義時の変数、引数(実引数;アーギュメント)は関数呼び出し時に渡す値です。

// x, yはパラメータ
func add(x int, y int) int {
	return x + y
}

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

同じ型のパラメータが続く場合、型の省略が可能です:

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

戻り値の基本

[編集]

関数は処理結果を戻り値として返すことができます。

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

複数の引数と戻り値

[編集]

複数の引数を持つ関数

[編集]

Goの関数は複数の引数を受け取ることができます。

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

複数の戻り値を返す関数

[編集]

Goは複数の値を同時に返すことができる特徴があります。

package main

import "fmt"

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

func main() {
	ary := [5][2]int{
		{10, 2},         // 正常な除算
		{10, 0},         // 除数がゼロ
		{0, 0},          // 0 ÷ 0 のケース
		{-10, 2},        // 負の数の除算
		{1000000000, 2}, // 大きな数での除算
	}
	for _, pair := range ary {
		fmt.Print(pair[0], " / ", pair[1])
		result, err := divide(pair[0], pair[1])
		if 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

名前付き戻り値

[編集]

Goでは戻り値に名前をつけることができます。これにより、関数内で戻り値の変数がゼロ値で初期化され、空の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() {
	// テストケースの配列
	ary := [5][2]int{
		{10, 2},         // 正常な除算
		{10, 0},         // 除数がゼロ
		{0, 0},          // 0 ÷ 0 のケース
		{-10, 2},        // 負の数の除算
		{1000000000, 2}, // 大きな数での除算
	}

	// テストケースをループで処理
	for _, pair := range ary {
		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 エラー。
  • テストケース: 配列 ary に以下のケースを用意しました:
    • 通常の除算
    • 除数が 0 の場合
    • 0 ÷ 0 の特殊ケース
    • 負の数での除算
    • 大きな数の除算
  • 出力の仕組み:
    • divide 関数を呼び出し、エラーがあればそれを表示。
    • エラーがなければ計算結果を表示。

関数の型

[編集]

関数の型宣言

[編集]

関数も型として扱うことができます。

package main

import "fmt"

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
}

関数を変数に代入

[編集]

関数は変数に代入して使用することができます。

package main

import "fmt"

func main() {
	square := func(n int) int {
		return n * n
	}

	fmt.Println(square(5)) // 出力: 25
}

関数を引数として渡す

[編集]

関数呼び出しを他の関数の引数として渡すことができます。

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) // 出力: 8
}

クロージャ

[編集]

クロージャとは

[編集]

クロージャは、外部のスコープにある変数を参照できる関数です。

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
}

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

[編集]

クロージャは状態を保持するために使用できます。

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() {
	fmt.Println(fibonacci()()) // 出力: 3
}

可変長引数

[編集]

可変長引数の基本

[編集]

可変長引数を使用すると、任意の数の引数を受け取ることができます。

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
}

スライスの展開

[編集]

スライスを可変長引数として渡す場合は、`...`を使用して展開します。

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
}

可変長引数の地雷

[編集]

Goの可変長引数の挙動には注意が必要です。特に、スライスのポインタ渡し値渡しの違いが混乱を招くことがあります。可変長引数を使った関数での挙動は、意図しない副作用を引き起こすこともあります。いくつかの重要な点を説明します。

可変長引数(...)の注意点

[編集]
  1. スライスのポインタ渡し 可変長引数は実際にはスライスとして扱われるため、スライスのポインタが関数に渡されます。これは、スライスが参照型であるため、関数内でスライスの内容を変更した場合、呼び出し元にも影響を与える可能性があるという意味です。
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 も影響を受けます。これはスライスがポインタで渡されるためです。
  1. 関数内での変更が予期しない副作用を生む 先程の例で示したように、関数内で可変長引数の要素を変更すると、呼び出し元のスライスにもその変更が反映されるため、副作用が発生する可能性があります。特に、他のコードがそのスライスを参照している場合、思わぬバグが発生することがあります。
  2. 引数として渡されたスライスはコピーされるわけではない Goの可変長引数は、引数がスライスとして渡されるため、引数の内部実装がポインタ型として渡されます。スライス自体がコピーされるわけではないため、関数内でスライスを変更すると、元のスライスが影響を受けます。
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 で新しいスライスを作成するため、呼び出し元の numbers は変更されません。

地雷となりやすいケース

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

対策

[編集]
  • 関数内でのスライスの変更を避ける: 必要がない限り、関数内でスライスを変更するのは避けるか、変更が呼び出し元に影響を与えないようにコピーを使う。
  • スライスを明示的にコピーする: 呼び出し元のデータを変更したくない場合は、スライスを関数内でコピーして使用する。
  • パラメータとして渡す前に十分な検討: 可変長引数で渡すデータの量や内容をよく検討し、大きなデータを渡す場合はスライスを直接渡すのではなく、ポインタやチャネルを使うことを検討する。

結論

[編集]

可変長引数(...)の機能は非常に便利ですが、副作用メモリ管理に関する理解が不十分だと、予期しない挙動やパフォーマンスの問題を引き起こす可能性があります。関数内で引数を変更する場合、その影響が呼び出し元にどのように伝わるかを十分に理解して使用する必要があります。

メソッド

[編集]

メソッドの定義

[編集]

メソッドは、特定の型に関連付けられた関数です。

package main

import "fmt"

type Rectangle struct {
	width, height float64
}

func (r Rectangle) Area() float64 {
	return r.width * r.height
}

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

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

[編集]

ポインタレシーバを使用すると、メソッド内で構造体を変更できます。

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}
	r.Scale(2)
	fmt.Println(r.Area()) // 出力: 200
}

defer文

[編集]

defer文の基本

[編集]

defer文は、関数の終了時に実行される処理を定義します。

func processFile(filename string) {
	f, err := os.Open(filename)
	if err != nil {
		return
	}
	defer f.Close() // 関数終了時にファイルを閉じる

	// ファイル処理
}

複数のdefer文の実行順序

[編集]

複数のdefer文は、LIFO(後入れ先出し)の順序で実行されます。

package main

import "fmt"

func main() {
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
	// 出力順序: 3, 2, 1
}

関数のベストプラクティス

[編集]

関数の命名規則

[編集]
  • 明確で説明的な名前を使用する
  • キャメルケースを使用する(最初の文字が大文字の場合はエクスポート可能)
  • 動詞または動詞句で始める

適切な関数の粒度

[編集]
  • 単一責任の原則に従う
  • 適切な長さを保つ(通常20-30行程度)
  • 必要に応じて分割する

ドキュメンテーション

[編集]

関数にはわかりやすいコメントを付け、godocのフォーマットに従います。

// Calculate returns the result of a mathematical operation on two integers.
// The operator parameter should be one of "+", "-", "*", or "/".
func Calculate(x, y int, operator string) (int, error) {
	// 実装
}

演習問題

[編集]
  1. 以下の仕様を満たす関数を作成してください:
    • 整数のスライスを受け取り、偶数の要素のみを含む新しいスライスを返す
    • エラー処理は考慮しない
  2. 次の関数を名前付き戻り値を使用してリファクタリングしてください:
    func divide(x, y float64) (float64, error) {
    	if y == 0.0 {
    		return 0, fmt.Errorf("division by zero")
    	}
    	return x / y, nil
    }
    
  3. クロージャを使用して、フィボナッチ数列の最初のn項を生成する関数を実装してください。

まとめ

[編集]

本章では、Goにおける関数の基本的な概念から高度な使用方法まで学びました:

  • 関数の基本的な構文と使用方法
  • 複数の戻り値と名前付き戻り値
  • 関数型とクロージャ
  • メソッドの定義と使用
  • defer文による後処理の実装
  • ベストプラクティスとコーディング規約

これらの知識を活用することで、より効率的で保守性の高いGoプログラムを作成することができます。