Golang 入門

Go 関数引数と戻り値

本章では、関数における極めて重要な 2 つの要素、引数 (Parameters)戻り値 (Return Values) について深く掘り下げます。データを効率的に関数へ渡し、その結果をどのように受け取るかを学習しましょう。

1. 関数引数の定義

関数引数とは、関数の定義の括弧内にリストアップされた変数のことです。これらにより外部データを関数内部へ渡すことができ、関数が呼び出されるたびに異なる入力値を処理することが可能になります。

1.1 基本的なパラメータ

引数を定義する最も直接的な方法は、関数の括弧内に引数名とデータ型を順番に記述することです。

package main

import "fmt"

// add は 2 つの整数型の引数 a と b を受け取り、それらの和を返します。
func add(a int, b int) int {
	return a + b
}

func main() {
	result := add(5, 3) // 引数 5 と 3 を使用して add 関数を呼び出す
	fmt.Println(result) // 出力: 8
}

この例では、add 関数は ab という 2 つの引数を受け取り、それらの型はどちらも int です。add(5, 3) を呼び出すと、数値 5a に、数値 3b に割り当てられます。

1.2 引数型の省略記法 (Shorthand)

連続する複数の引数が同じデータ型を持つ場合、それぞれの型を繰り返し書く必要はありません。一連の引数の最後にだけ型を記述します。

package main

import "fmt"

// multiply は 2 つの整数型の引数 a と b を受け取り、それらの積を返します。
// 同じ型の引数における省略記法の宣言方法に注目してください。
func multiply(a, b int) int {
	return a * b
}

func main() {
	product := multiply(4, 6) // 引数 4 と 6 を使用して multiply 関数を呼び出す
	fmt.Println(product)      // 出力: 24
}

ここでは、a, b inta int, b int と完全に等価です。この省略記法は、特に同じ型の引数が長く続く場合にコードの可読性を大幅に向上させます。

1.3 値渡しのメカニズム (Passing by Value)

Go 言語では、引数は「値渡し (Pass by Value)」によって渡されます。これは、変数を関数に渡す際、Go がメモリ上でその変数の値をコピーし、関数内部ではそのコピー(レプリカ)を使用することを意味します。したがって、関数内部で引数に対して行われたいかなる変更も、関数外部の元の変数には一切影響を与えません。

package main

import "fmt"

// modifyValue は渡された引数の値を変更しようと試みます。
func modifyValue(x int) {
	x = x * 2 // 関数内部で x の値を変更
	fmt.Println("modifyValue 内部:", x) // 出力: modifyValue 内部: 20
}

func main() {
	num := 10
	modifyValue(num) // num を modifyValue 関数に渡す
	fmt.Println("modifyValue 外部:", num) // 出力: modifyValue 外部: 10
}

この例では、nummodifyValue に渡されます。関数内部の x は単なる num のコピーです。x の値を変更しても、main 関数内の num の値が変わることはありません。このデータ処理の仕組みを理解することは、Go の関数をマスターする上で極めて重要です。

1.4 引数の無視

関数のシグネチャ(Signature)上は引数が必要であっても、関数本体でそれを使用しない場合があります。そのような状況では、ブランク識別子 _ (アンダースコア) を使用してその引数を無視できます。

package main

import "fmt"

// greet は渡された name 引数を無視しますが、関数のシグネチャ要件は満たしています。
func greet(_ string) {
	fmt.Println("こんにちは!")
}

func main() {
	greet("Alice") // 引数を渡していますが、関数内では無視されます
}

ここでは、name 引数が宣言されていますが使用されていません。ブランク識別子 _ を使用することで、Go コンパイラのエラー(Go は宣言した不使用変数に対して非常に厳格です)を防ぐことができます。これは、特定の関数シグネチャを強制するインターフェース (Interfaces) を実装する際などに非常に役立ちます。

2. 関数の戻り値を定義する

関数は実行結果を呼び出し元に返すことができます。戻り値の型は、関数の引数リストの後に宣言する必要があります。

2.1 単一の戻り値

最も一般的なケースは、関数が 1 つの値を返す場合です。

package main

import "fmt"

// square は整数の二乗を計算し、その結果を返します。
func square(x int) int {
	return x * x
}

func main() {
	result := square(7) // 引数 7 を使用して square 関数を呼び出す
	fmt.Println(result) // 出力: 49
}

この例では、square は整数 x を受け取り、その二乗(これも整数)を返します。

2.2 多値戻り値

Go 言語には非常に強力な特徴があります。それは、関数が同時に複数の値を返すことを許可している点です。これは Go において、主に「実行結果」と「エラー状態」を同時に返すために頻繁に利用されます。

package main

import (
	"fmt"
	"errors"
)

// divide は整数の除算を行い、商と余りを返します。
// 除数がゼロの場合、エラーも返します。
func divide(numerator, denominator int) (int, int, error) {
	if denominator == 0 {
		return 0, 0, errors.New("ゼロで割ることはできません")
	}
	quotient := numerator / denominator
	remainder := numerator % denominator
	
	// 複数の値を返す:商、余り、そしてエラーがないことを示す nil
	return quotient, remainder, nil 
}

func main() {
	// divide 関数を呼び出し、複数の戻り値を受け取る
	quotient, remainder, err := divide(10, 3) 
	if err != nil {
		fmt.Println("エラーが発生しました:", err)
		return
	}
	fmt.Println("商:", quotient)   // 出力: 商: 3
	fmt.Println("余り:", remainder) // 出力: 余り: 1
	
	// ブランク識別子 _ を使用して商と余りを無視し、エラーの有無だけを確認する
	_, _, err = divide(5, 0)
	if err != nil {
		fmt.Println("エラーが発生しました:", err) // 出力: エラーが発生しました: ゼロで割ることはできません
		return
	}
}

上記のコードでは、divide は商、余り、そして error の 3 つの値を返しています。除算が成功すればエラー値は nil になり、除数がゼロであればエラーオブジェクトが返されます。これにより、呼び出し元は非常に便利にエラーをチェックして処理できます。

2.3 名前付き戻り値

Go では、関数のシグネチャの中で戻り値に直接名前を付けることができます。これにより、特に複数の値を返す際のコードの可読性が向上します。

名前付き戻り値は、関数の先頭で宣言されたローカル変数として扱われます。その後、「裸の return (Naked return)」 ステートメント(return の後ろに何も書かない形式)を使用すると、名前を付けたこれらの変数の現在の値が自動的に返されます。

package main

import "fmt"

// calculateAreaAndPerimeter は長方形の面積と周囲の長さを計算します。
// 明確にするために、名前付き戻り値を使用しています。
func calculateAreaAndPerimeter(length, width float64) (area, perimeter float64) {
	area = length * width
	perimeter = 2 * (length + width)
	return // 裸の return ステートメント。area と perimeter の現在の値を自動的に返します
}

func main() {
	a, p := calculateAreaAndPerimeter(5.0, 4.0)
	fmt.Println("面積:", a) // 出力: 面積: 20
	fmt.Println("周囲の長さ:", p) // 出力: 周囲の長さ: 18
}

名前付き戻り値は可読性を高める場合がありますが、関数のロジックが複雑な場合、乱用すると「最終的に何が返されているのか」が不透明になる可能性があります。状況に応じて適切に使い分ける必要があります。

2.4 戻り値の無視

引数の無視と同様に、多値戻り値の関数を呼び出した際、特定の結果のみに興味がある場合は、ブランク識別子 _ を使用して不要な戻り値を破棄できます。

package main

import "fmt"

// getCoordinates は x と y 座標を返します。
func getCoordinates() (int, int) {
	return 10, 20
}

func main() {
	x, _ := getCoordinates() // 2 番目の戻り値 (y 座標) を無視
	fmt.Println("X 座標:", x) // 出力: X 座標: 10
}

3. 実戦ケーススタディ

様々なシナリオで関数引数と戻り値をどのように活用するか、実戦的な例で見ていきましょう。

3.1 ケース 1:数値グループの平均値を計算する

この例では、スライス (Slice) を引数として関数に渡し、計算された平均値を返す方法を示します。

package main

import "fmt"

// calculateAverage は浮動小数点数のスライスの平均値を計算します。
func calculateAverage(numbers []float64) float64 {
	if len(numbers) == 0 {
		return 0 // スライスが空の場合、ゼロ除算を避けるために 0 を返す
	}
	sum := 0.0
	for _, number := range numbers {
		sum += number
	}
	return sum / float64(len(numbers))
}

func main() {
	values := []float64{10.0, 20.0, 30.0, 40.0, 50.0}
	average := calculateAverage(values) // スライスを渡して呼び出し
	fmt.Println("平均値:", average)      // 出力: 平均値: 30
}

3.2 ケース 2:温度単位の変換

摂氏(Celsius)と華氏(Fahrenheit)の間で相互変換を行う例です。

package main

import "fmt"

// celsiusToFahrenheit は摂氏を華氏に変換します。
func celsiusToFahrenheit(celsius float64) float64 {
	return (celsius * 9 / 5) + 32
}

// fahrenheitToCelsius は華氏を摂氏に変換します。
func fahrenheitToCelsius(fahrenheit float64) float64 {
	return (fahrenheit - 32) * 5 / 9
}

func main() {
	celsius := 25.0
	fahrenheit := celsiusToFahrenheit(celsius) 
	fmt.Printf("%.2f°C は %.2f°F です\n", celsius, fahrenheit) // 出力: 25.00°C は 77.00°F です
	
	fahrenheit = 77.0
	celsius = fahrenheitToCelsius(fahrenheit) 
	fmt.Printf("%.2f°F は %.2f°C です\n", fahrenheit, celsius) // 出力: 77.00°F は 25.00°C です
}

3.3 ケース 3:簡易計算機のシミュレーション

シンプルな計算機を作成する例です。除算関数では多値戻り値を利用してエラーハンドリングを行っています。

package main

import (
	"fmt"
	"errors"
)

// add は加算を行います。
func add(a, b float64) float64 { return a + b }

// subtract は減算を行います。
func subtract(a, b float64) float64 { return a - b }

// multiply は乗算を行います。
func multiply(a, b float64) float64 { return a * b }

// divide は除算を行い、除数がゼロの場合はエラーを返します。
func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("ゼロで割ることはできません")
	}
	return a / b, nil
}

func main() {
	num1 := 10.0
	num2 := 5.0
	
	fmt.Println("加算結果:", add(num1, num2))       // 出力: 加算結果: 15
	fmt.Println("減算結果:", subtract(num1, num2))  // 出力: 减算結果: 5
	fmt.Println("乗算結果:", multiply(num1, num2))  // 出力: 乗算結果: 50
	
	quotient, err := divide(num1, num2) 
	if err != nil {
		fmt.Println("エラー:", err)
	} else {
		fmt.Println("除算結果:", quotient) // 出力: 除算結果: 2
	}
	
	// ゼロ除算を試みる
	_, err = divide(num1, 0) 
	if err != nil {
		fmt.Println("エラー:", err) // 出力: エラー: ゼロで割ることはできません
	}
}