Go関数
この章では、Goの関数について概要を解説し、ハンズオン形式で実際に動作させながら学習します。これにより、関数の定義と呼び出し、多値返却、error の扱い方、匿名関数・クロージャ、init 関数、そして参照型で共通に使われる nil の考え方など、Goで書くコードの基本になる要素を身につけます。
1. 本章の概要
1.1 本章の目的
関数は、処理をまとまりとして切り出し、名前を通して再利用するための仕組みです。本章では、関数の定義(引数・戻り値)を基本として押さえたうえで、可変長引数、多値返却、error の扱い方、関数の応用的な使い方(匿名関数・クロージャ)、init 関数、そして「値が設定されていない」ことを示す nil の考え方までを扱います。
1.2 ハンズオンの流れ
本章では、まず関数の定義・引数・戻り値の書き方を押さえます。そのうえで、可変長引数・多値返却・匿名関数・クロージャ・init 関数などのGoらしい書き方を1つずつ動かしながら確認します。
1.3 事前準備
必要なツール
この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。
| ツール名 | 関連箇所 | 理由 |
|---|---|---|
| Visual Studio Code | Visual Studio Codeのインストール | Goのコードを記述するエディタとして使用する |
| Go | Goのインストール | 本章のサンプルコードを実行する言語処理系として使用する |
2. 関数
同じ計算をコード内で何度も書く場面はよくあります。たとえば「2つの数を足して表示する」処理を3回行うと、次のようになります。
package main
import "fmt"
func main() {
result1 := 3 + 5
fmt.Println(result1)
result2 := 10 + 20
fmt.Println(result2)
result3 := 100 + 200
fmt.Println(result3)
}
値が変わるだけで、処理の中身はどれも同じ「足して表示する」の繰り返しです。回数が増えるほどコードが冗長になり、あとから計算式を変更したいときにも直す箇所が増えてしまいます。
このような繰り返しを解決するのが関数です。処理をひとまとまりとして切り出して名前を付けておけば、同じロジックを1回書くだけで、引数を変えながら何度でも呼び出せます。
2.1 関数の定義
Goの関数は func キーワードで定義します。まずはもっともシンプルな「引数も戻り値もない関数」から見ていきます。書き方は以下のとおりです。
func 関数名() {
// 処理
}
実際に試してみましょう。先ほどの冒頭では 3 + 5 を書いて表示する処理を並べていましたが、まずはそのうちの1つを関数として切り出してみます。次のコードは、add という関数を定義し、3 + 5 の結果を表示するコードです。main.go を以下の内容に書き換えて保存し、go run main.go で実行します。
package main
import "fmt"
func add() {
fmt.Println(3 + 5)
}
func main() {
add()
}
以下のような実行結果が表示されます。
8
main から add() を呼び出し、add 関数の中の fmt.Println(3 + 5) が実行されて結果 8 が出力されました。
コードを解説します。
func add() {
fmt.Println(3 + 5)
}
func 関数名() { ... } の形で関数を定義します。ここでは add という名前のブロックに fmt.Println(3 + 5) をまとめています。
add()
関数名() の形で関数を呼び出します。呼び出された関数の中の処理が順に実行されます。
なお、これまでのサンプルにも登場している main 関数も、引数と戻り値を持たないこの形の関数です。ただし main は特別扱いされていて、プログラムを実行したときに最初に呼び出される「エントリポイント」として動きます。
引数
先ほどの add は 3 + 5 を固定で計算するだけでした。これでは、冒頭に挙げた 10 + 20 や 100 + 200 には使い回せません。関数の外から値を渡して使えるようにする仕組みが引数です。カッコの中に 引数名 型 の形で書きます。
func 関数名(引数名 型) {
// 処理
}
次のコードは、渡した2つの整数を足して表示する add 関数の例です。1つの関数で3ケースをまとめて処理できます。
package main
import "fmt"
func add(a int, b int) {
fmt.Println(a + b)
}
func main() {
add(3, 5)
add(10, 20)
add(100, 200)
}
以下のような実行結果が表示されます。
8
30
300
3回の呼び出しで、それぞれ渡した値の合計が表示されました。冒頭の冗長な書き方が、add 一つに整理できています。
コードを解説します。
func add(a int, b int) {
fmt.Println(a + b)
}
引数 a・b(どちらも int 型)を受け取れる add 関数として定義しています。関数の中では a と b を通常の変数として使えます。
add(3, 5)
関数名(値1, 値2) の形で呼び出すと、値が順に引数に渡されます。ここでは 3 が a、5 が b に入って関数が実行されます。
同じ型の引数が続く場合、型宣言をまとめて省略できます。以下は同じ意味です。
func add(a, b int) {
// 処理
}
a int, b int を a, b int に短縮しています。実際にはこの短縮形が多く使われます。
戻り値
これまでの add は「計算して表示する」までを関数の中でやっていました。しかし、計算結果を別の処理に使いたい場合(変数に入れて別のところで参照する、他の計算に使うなど)は、関数から値を返してもらう必要があります。そのための仕組みが戻り値です。引数のあとに戻り値の型を書き、return で値を返します。書き方は以下のとおりです。
func 関数名(引数名 型) 戻り値の型 {
// 処理
return 戻り値
}
次のコードは、2つの整数を受け取って足し算した結果を返す add 関数を定義し、3ケースで呼び出して結果を表示するコードです。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
result1 := add(3, 5)
fmt.Println(result1)
result2 := add(10, 20)
fmt.Println(result2)
result3 := add(100, 200)
fmt.Println(result3)
}
以下のような実行結果が表示されます。
8
30
300
3回の呼び出しで、それぞれ 3+5、10+20、100+200 の結果が返され、変数に代入されて出力されました。
コードを解説します。
func add(a, b int) int {
return a + b
}
引数のあとの int が戻り値の型で、この関数は int を1つ返します。関数の中では return に返したい値を書きます。ここでは a + b の計算結果を返しています。
result1 := add(3, 5)
関数が返す値は、呼び出しの結果を変数に代入して受け取ります。add(3, 5) の戻り値 8 が result1 に入ります。同じ形で add(10, 20) の結果は result2、add(100, 200) の結果は result3 に入り、それぞれ fmt.Println で表示されます。
3. 可変長引数
「同じ種類の値を、個数を決めずに関数に渡したい」場面はよくあります。たとえば「渡された整数をすべて足し合わせる sum 関数」を作りたいときです。
これまで学んだ書き方だと、事前にスライスを組み立ててから関数に渡すことになります。
package main
import "fmt"
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum([]int{1, 2, 3}))
fmt.Println(sum([]int{10, 20, 30, 40, 50}))
}
以下のような実行結果が表示されます。
6
150
動きはしますが、呼び出す側で毎回 []int{...} とスライスを作るのが少し冗長です。
このような場面のために、関数の最後の引数を ...型 と書くと、任意の個数の値を直接受け取れる可変長引数の書き方が用意されています。書き方は以下のとおりです。
func 関数名(引数名 ...型) 戻り値の型 {
// 処理(引数名は []型 として扱える)
}
先ほどの sum を可変長引数で書き直すと、次のようになります。
package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3))
fmt.Println(sum(10, 20, 30, 40, 50))
}
以下のような実行結果が表示されます。
6
150
同じ結果が得られました。呼び出し側は sum(1, 2, 3) のように、値を直接カンマ区切りで渡すだけで済むようになっています。
コードを解説します。
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
nums ...int は「任意の個数の int 型引数を受け取る」の意味で、関数内では nums は []int として扱われます。中の処理は先ほどと同じで、for-range で1つずつ取り出して total に加算しています。
fmt.Println(sum(1, 2, 3))
fmt.Println(sum(10, 20, 30, 40, 50))
呼び出し側は、引数をカンマ区切りで任意の個数渡せます。fmt.Println や fmt.Printf も可変長引数を使った関数の一例です。
4. 多値返却
Goの関数は、複数の値を同時に返せます。これはGoの大きな特徴で、割り算の商と余りを1つの関数で得たり、成功した値と error を同時に返したりできます。
4.1 基本の書き方
戻り値の型をカンマ区切りでカッコに並べ、return に複数の値を書くと、複数の値を同時に返せます。書き方は以下のとおりです。
func 関数名(引数名 型) (戻り値1の型, 戻り値2の型) {
// 処理
return 値1, 値2
}
次のコードは、割り算の商と余りを1つの関数で同時に返す例です。
package main
import "fmt"
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
func main() {
q, r := divide(10, 3)
fmt.Printf("商=%d, 余り=%d\n", q, r)
}
以下のような実行結果が表示されます。
商=3, 余り=1
10 ÷ 3 の商 3 と余り 1 が、それぞれ q と r に受け取られて表示されました。
コードを解説します。
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
戻り値の型を (int, int) のようにカッコで囲み、複数書くことで多値返却関数になります。関数の中では return に複数の値をカンマ区切りで書きます。ここでは商と余りの両方を返しています。
q, r := divide(10, 3)
呼び出し側は、戻り値を対応する数の変数で受け取ります。q に商 3、r に余り 1 が代入されます。
4.2 エラーを返す慣用パターン
多値返却の最も重要な使い道が、成功時の値とエラーを同時に返すパターンです。Goでは例外機構ではなくこの形でエラーを扱うのが基本です。書き方は以下のとおりです。
func 関数名(引数名 型) (戻り値の型, error) {
if エラー条件 {
return ゼロ値, errors.New("エラーメッセージ")
}
return 正常時の値, nil
}
呼び出し側は 結果, err := 関数名(...) の形で受け取り、if err != nil でエラーの有無を判定します。次のコードは、0 で割ろうとしたときにエラーを返す divide を定義し、呼び出し側でエラーを判定して表示する例です。
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("0で割ることはできません")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
}
以下のような実行結果が表示されます。
エラー: 0で割ることはできません
divide(10, 0) はゼロ除算になるためエラー値を返し、呼び出し側の if err != nil の分岐に入って エラー: 0で割ることはできません が表示されました。
コードを解説します。
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("0で割ることはできません")
}
return a / b, nil
}
戻り値の型を (int, error) としています。error は Go の組み込み型で、エラー情報を表します。b == 0 のときは 0 と errors.New("...") で作ったエラー値を返し、そうでなければ計算結果と nil(エラーなし)を返します。
result, err := divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
return
}
呼び出し側は結果とエラーの両方を受け取り、まず if err != nil でエラーの有無を判定します。エラーがあれば処理して return で抜け、なければ結果を使う、という流れがGoのエラーハンドリングの慣用句で、コードを読むときに最も頻繁に登場するパターンです。
なお、成功時に返している nil は、Go で「値がない/何も指していない」ことを示す特別な値です。本章末尾の「## nil」節で詳しく扱います。
4.3 名前付き戻り値
戻り値に名前を付けておくと、関数の中で変数として使え、return に値を書かないだけで自動的にそれらが返されます。書き方は以下のとおりです。
func 関数名(引数名 型) (戻り値1の名前 型1, 戻り値2の名前 型2) {
戻り値1の名前 = ...
戻り値2の名前 = ...
return
}
次のコードは、名前付き戻り値を使って divide を書き直した例です。
package main
import "fmt"
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return
}
func main() {
q, r := divide(10, 3)
fmt.Printf("商=%d, 余り=%d\n", q, r)
}
以下のような実行結果が表示されます。
商=3, 余り=1
多値返却版と同じ結果が得られています。名前付き戻り値でも、呼び出し側の受け取り方は変わりません。
コードを解説します。
func divide(a, b int) (quotient, remainder int) {
戻り値の型に quotient と remainder という名前を付けています。これらは関数の中で普通の変数として扱えます。
quotient = a / b
remainder = a % b
return
quotient と remainder に値を代入し、引数のない return を書くだけで、それらの値が戻り値として返されます。これをネイキッドリターンと呼びます。短い関数では読みやすさが上がりますが、長い関数で使うと分かりにくくなるため、使い分けの判断が必要です。
5. 関数の応用的な使い方
これまで見てきた「名前を付けて定義して呼び出す」書き方だけでなく、Goには関数を柔軟に使うための書き方がいくつか用意されています。ここでは、関数を変数に代入して受け取る書き方、名前を付けずにその場で書く匿名関数、そして外側の変数を保持できるクロージャの3つを扱います。
いずれも「Goでは関数も値として扱える」という性質(第一級関数)を土台にした書き方で、実務で書く柔軟なコードや、後の章で扱う応用的な処理でも度々登場します。
5.1 値としての関数
Goでは、関数を「値」として扱えるため、他の値と同じように変数に代入したり、関数の引数として渡したり、戻り値として返したりできます。ここではその3つの書き方を順に見ていきます。
変数への代入
func で定義した関数は、() を付けずに書くと「関数そのもの」を値として扱えます。書き方は以下のとおりです。
変数名 := 関数名 // () を付けないと関数そのものが代入される
変数名(引数) // 代入した変数を関数として呼び出す
次のコードは、add 関数を f という変数に代入し、f(3, 5) で呼び出す例です。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
f := add
fmt.Println(f(3, 5))
}
以下のような実行結果が表示されます。
8
f に add を代入したことで、f(3, 5) は add(3, 5) と同じ意味になり、8 が出力されました。
コードを解説します。
f := add
add に () を付けずに書くと、「add という関数そのもの」を値として扱えます。ここでは f にその関数を代入しています。f の型は func(int, int) int になります。
fmt.Println(f(3, 5))
f に入っている関数を f(3, 5) の形で呼び出しています。呼び出し方は元の add(3, 5) と同じです。
引数として渡す
関数を変数に入れられるということは、他の関数の引数としても渡せます。書き方は以下のとおりです。引数の位置に func(引数の型) 戻り値の型 の形で「関数を受け取る型」を書きます。
func 関数名(引数名 func(引数の型) 戻り値の型, その他の引数) {
引数名(その他の引数) // 渡された関数を呼び出す
}
次のコードは、2つの整数を計算する関数を引数として受け取り、それを実行する apply 関数の例です。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func apply(op func(int, int) int, a, b int) int {
return op(a, b)
}
func main() {
result := apply(add, 3, 5)
fmt.Println(result)
}
以下のような実行結果が表示されます。
8
apply が受け取った add を内部で op(3, 5) として呼び出し、8 が返って表示されました。
コードを解説します。
func apply(op func(int, int) int, a, b int) int {
return op(a, b)
}
op func(int, int) int は「int を2つ受け取って int を返す関数」を表す型で、この位置に add のような関数を受け取れます。
result := apply(add, 3, 5)
apply の呼び出しに add(関数そのもの)と 3, 5(add に渡す値)を渡しています。apply の中で op(3, 5) として呼び出されるので、実質的に add(3, 5) が実行されます。
引数として関数を受け取る書き方は、並び替えのルールを渡したり、状況ごとに処理を差し込んだりする場面で頻繁に登場します。
戻り値として返す
関数は戻り値としても返せます。書き方は以下のとおりです。戻り値の型に func(引数の型) 戻り値の型 の形で「返す関数の型」を書きます。
func 関数名(引数) func(引数の型) 戻り値の型 {
return 別の関数 // 条件に応じて返す関数を切り替えられる
}
次のコードは、名前に応じて add か sub を返す getOperation 関数の例です。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func getOperation(name string) func(int, int) int {
if name == "add" {
return add
}
return sub
}
func main() {
op := getOperation("add")
fmt.Println(op(3, 5))
}
以下のような実行結果が表示されます。
8
getOperation("add") が add 関数を返し、それを op で受け取って op(3, 5) を呼び出したところ、8 が出力されました。
コードを解説します。
func getOperation(name string) func(int, int) int {
if name == "add" {
return add
}
return sub
}
戻り値の型 func(int, int) int は「int を2つ受け取って int を返す関数」を表します。中身では条件に応じて add と sub を返し分けています。
op := getOperation("add")
fmt.Println(op(3, 5))
getOperation("add") が返した関数を op に受け取り、通常の関数と同じように op(3, 5) の形で呼び出せます。
このように、関数を戻り値として返すと、状況に応じて振る舞いを切り替える処理を簡潔に書けます。次項で扱うクロージャも、この「関数を戻り値として返す」書き方の応用です。
5.2 匿名関数
名前を付けずにその場で関数を書くこともできます。これを匿名関数(または関数リテラル)と呼びます。書き方は以下のとおりです。
func(引数名 型) 戻り値の型 {
// 処理
}
func のあとに名前を書かず、そのまま () で引数を並べます。定義した匿名関数は、そのまま変数に代入したり、直後に呼び出したりできます。次のコードは、匿名関数を作ってその場で呼び出す例です。
package main
import "fmt"
func main() {
result := func(a, b int) int {
return a + b
}(3, 5)
fmt.Println(result)
}
以下のような実行結果が表示されます。
8
匿名関数を定義した直後の (3, 5) で呼び出しが行われ、8 が返って result に入りました。
コードを解説します。
result := func(a, b int) int {
return a + b
}(3, 5)
func(a, b int) int { ... } が匿名関数の定義で、その直後の (3, 5) が呼び出しです。名前を付けないので、その場で1回だけ使う小さな処理を書きたいときに便利です。
匿名関数は、名前を付けて別に定義するほどでもない小さな処理を、その場で書きたいときによく使います。たとえば前項の apply のように「関数を引数として受け取る関数」に渡す値を、事前に名前付き関数を用意せず、apply(func(a, b int) int { return a + b }, 3, 5) のようにそのまま書けます。
5.3 クロージャ
匿名関数の中から、外側のスコープの変数を参照できます。参照した変数は関数と一緒に閉じ込められる(=クロージャする)ため、この仕組みをクロージャと呼びます。次のコードは、呼び出すたびに1ずつ増えるカウンタを作る例です。
package main
import "fmt"
func newCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
counter := newCounter()
fmt.Println(counter())
fmt.Println(counter())
fmt.Println(counter())
}
以下のような実行結果が表示されます。
1
2
3
counter() を呼び出すたびに、内部の count が保持されたまま1ずつ増えて出力されました。
コードを解説します。
func newCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
newCounter は「引数なし・int を返す関数(func() int)」を戻り値として返す関数です。中で宣言した count を、返す匿名関数の中から参照しています。匿名関数と一緒に count も閉じ込められるため、newCounter の呼び出しが終わった後も count は保持され続けます。
counter := newCounter()
fmt.Println(counter())
fmt.Println(counter())
newCounter() の戻り値の関数を counter に受け取り、counter() を呼び出すごとに内部の count が更新されます。1回目は 1、2回目は 2、3回目は 3 が返ります。
クロージャは、状態を持つ小さな関数を作りたいとき(IDの採番、キャッシュ、イベントハンドラなど)でよく使われます。
6. init 関数
init は、プログラムが起動したときに自動的に1回だけ実行される特別な関数です。main 関数が呼ばれる前に、宣言されているすべての init が順に実行されます。書き方は以下のとおりです。
func init() {
// 初期化処理
}
引数も戻り値も持たず、明示的に呼び出す必要はありません。次のコードは、init と main の実行順を確かめる例です。
package main
import "fmt"
func init() {
fmt.Println("initが実行された")
}
func main() {
fmt.Println("mainが実行された")
}
以下のような実行結果が表示されます。
initが実行された
mainが実行された
main より先に init が実行されていることが分かります。
コードを解説します。
func init() {
fmt.Println("initが実行された")
}
init はGoが特別扱いする名前で、関数を定義するだけで自動的に呼び出されます。プログラムを開始する前にやっておきたい初期化処理(設定値の準備、外部ライブラリの登録など)を書くのが典型的な用途です。
init は同じプログラム内に複数書くこともでき、宣言された順に実行されます。ただし、多用するとどこで何が初期化されているか分かりにくくなるため、必要な場面に絞って使うのが原則です。
7. nil
「エラーを返す慣用パターン」節では、成功時に return nil と書いてきました。ここで登場する nil(ニル)は、Go で「値が設定されていない/何もない」ことを示す特別な値です。
nil は Go の予約語で、値を間接的に指し示す種類の型(参照型)で共通して使われます。以下のような型が該当します。
いずれの型も「間接的に何かを指し示す型」で、指す先が何もない状態を nil で表す、というのが共通の考え方です。
7.1 nil の判定
nil かどうかは、== や != で直接比較して判定します。書き方は以下のとおりです。
if 変数名 == nil {
// 値が nil のときの処理
}
if 変数名 != nil {
// 値が nil でないときの処理
}
前節までで見た if err != nil も、このパターンの一例です。関数から返ってきた err が nil かどうか(=エラーが発生したかどうか)を判定して、失敗時のみエラー処理に進む書き方になっています。
7.2 他の型でのnil
nil はどの型でも「初期化されていない/どこも指していない」ことを表しますが、nil のまま操作しようとしたときの挙動は型ごとに違います。
| 型 | nil に関する挙動・使い方 |
|---|---|
interface(error を含む) |
nil かどうかで「エラーが発生したか」等の判定に使う |
| ポインタ | 参照(デリファレンス)すると実行時エラーで停止する |
| スライス | len(s) は 0 を返し、append もそのまま動くため空スライスと同等に扱える |
| マップ | 読み取りは可能だが、キーへ書き込むと実行時エラーで停止する |
| チャネル | 送信・受信すると処理がその場で止まる |
| 関数変数 | 呼び出すと実行時エラーで停止する |
各型の具体的な使い方は、対応する章(Goポインタ・Goインタフェース・Goデータ構造・Goの並行処理 など)で扱います。共通するのは「未初期化のときは nil」「参照する前に nil チェックする」の考え方です。
8. まとめ
この章では、Goの関数を学びつつ、実際に関数の定義・呼び出し・可変長引数・多値返却・匿名関数・クロージャ・init・nil を体験しました。
- 関数は
funcで定義し、引数と戻り値の型を宣言する - 引数の外から値を渡し、
returnで結果を呼び出し元に返せる - 最後の引数を
...型にすると任意個数の引数を受け取れる(可変長引数) - 戻り値をカッコに複数並べると同時に返せる(多値返却)
(結果, error)を返し、呼び出し側でif err != nilを判定するのが、Goの慣用エラー処理である- 関数は変数に代入したり匿名関数として書けたりする(第一級関数)。外側の変数を閉じ込めるとクロージャになる
initはmainより前に自動実行される特別な関数で、プログラム開始前の初期化処理に使うnilは「値が設定されていない/何も指していない」ことを示す Go の予約語で、interface(errorを含む)・ポインタ・スライス・マップ・チャネル・関数変数といった参照型で共通して使われる
次の章では、Goの構造体を学びつつ、実際にstructでデータと振る舞いをまとめる書き方を体験します。