Go基礎 #4 関数、多値返却、error型

読了 6分

#3 制御フロー で分岐と繰り返しを見ました。今回は — そのコードを束ねて再利用する道具。Goの関数。

関数定義 #

基本関数
func add(a int, b int) int {
	return a + b
}

func main() {
	result := add(2, 3)
	fmt.Println(result)   // 5
}

文法: func 名前(仮引数) 戻り値型 { 本体 }。仮引数ごとに型を書きます。

同じ型の仮引数をまとめる #

型をまとめる
func add(a, b int) int {
	return a + b
}

同じ型が連続するなら最後に一度だけ書きます。

戻り値なし #

void のような関数
func greet(name string) {
	fmt.Println("こんにちは,", name)
}

戻り値の型を空にします。Cの void と同じ位置付け。

多値返却 — Goのシグネチャ #

Goの最も目立つ特徴 — 関数が複数の値を一度に返却できます。

多値返却
func divide(a, b int) (int, int) {
	q := a / b
	r := a % b
	return q, r
}

func main() {
	quot, rem := divide(10, 3)
	fmt.Println(quot, rem)   // 3 1
}

JSのデストラクチャリングやPythonのタプル分解に似ていますが — Goは本当の多値です。呼び出し側で2つの変数に同時に受け取ります。

一部を無視 — _ #

無視
quot, _ := divide(10, 3)   // 余りは無視

_ は「この値は使わない」の意味。可読性が良く、未使用変数のコンパイルエラーも避けられます。

名前付き戻り値 #

戻り値にあらかじめ名前を付けられます。

名前付き戻り値
func divide(a, b int) (q, r int) {
	q = a / b
	r = a % b
	return    // q, r 自動使用 (naked return)
}

qr が関数開始時に自動で宣言され、return に引数がなければその値が自動で返却されます。

可読性の面で好みが分かれます。短い関数で明確さが増すならOK、長くなれば明示的な return q, r の方が良いです。

error 型 — Goのエラー処理 #

他の言語がtry/catchで例外を投げる場面で、Goはerrorを戻り値として扱います。

エラーを戻り値として
import (
	"fmt"
	"strconv"
)

func main() {
	n, err := strconv.Atoi("42")
	if err != nil {
		fmt.Println("失敗:", err)
		return
	}
	fmt.Println(n)
}

Atoi は2つの値を返します — 変換された整数とerror。errorがnilなら成功、nilでなければその中にエラー情報。

if err != nil { ... } パターンはGoコードの半分くらいと言っても過言ではありません。

errorはインターフェース #

error の正体
type error interface {
	Error() string
}

error はビルトインインターフェースで、Error() string メソッド一つだけを持ちます。どんな型でもこのメソッドがあればerrorになります。

自分の関数からエラーを返す #

エラーを作って返す
import "errors"

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("0で割ることはできません")
	}
	return a / b, nil
}

func main() {
	r, err := divide(10, 0)
	if err != nil {
		fmt.Println("エラー:", err)
		return
	}
	fmt.Println(r)
}

errors.New(...) が最もシンプルなエラー生成。メッセージに値を埋め込みたければ fmt.Errorf の方が一般的です。

fmt.Errorf
return 0, fmt.Errorf("0で割ろうとしている a=%d", a)

Sentinelエラー — あらかじめ定義されたエラー #

ライブラリがよく使うパターン — あらかじめ変数として定義しておいたエラー。

sentinel
var ErrNotFound = errors.New("not found")

func lookup(key string) (string, error) {
	if key == "" {
		return "", ErrNotFound
	}
	return "value", nil
}

// 呼び出し側が比較可能
if errors.Is(err, ErrNotFound) {
	// ...
}

errors.Is中級 #2 エラー処理 で詳しく扱います。

defer — 関数が終わる直前に実行 #

リソースの後始末(ファイルを閉じる、ロック解除、DB接続を閉じる)にほぼ標準で使う道具。

defer 基本
func readFile() {
	file, err := os.Open("data.txt")
	if err != nil {
		return
	}
	defer file.Close()   // 関数終了時に自動で呼び出される

	// ... ファイルを使用
}

defer はその行をすぐ実行するのではなく、関数がreturnする直前に実行されるよう予約します。どんな経路(正常return、panicなど)で関数を抜けても呼び出されます。

複数のdeferはLIFO #

LIFO 順序
func main() {
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")

	fmt.Println("main")
}
// main
// 3
// 2
// 1

スタックのように — 後に書いたものが先に実行されます。

deferの引数は即時評価される #

よく混乱する罠。

引数は即時評価
i := 10
defer fmt.Println(i)   // 引数 i がこの時点で評価 → 10
i = 20

// 関数終了 — 出力: 10

fmt.Println(i) のiがdefer時点で評価され10で固定されます。その後にiが変わっても影響ありません。

これを避けたければクロージャで包みます。

クロージャで遅延
i := 10
defer func() { fmt.Println(i) }()    // 実行時点で i を評価
i = 20
// 出力: 20

可変長引数 — ... #

variadic 関数
func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

sum(1, 2)              // 3
sum(1, 2, 3, 4, 5)     // 15
sum()                   // 0

nums ...int — 任意の数のintを受け取りスライスにまとめる。JSのrest仮引数 (基礎 #4) と同じ位置付けです。

スライスを展開して渡す #

すでにスライスがあれば — ... で展開して引数として渡せます。

スライス展開
nums := []int{1, 2, 3}
sum(nums...)   // 6 — スライスを引数として展開

関数も値 (第一級オブジェクト) #

関数を変数に入れたり、他の関数に引数として渡したり、返却したりできます。

関数値
add := func(a, b int) int {
	return a + b
}

result := add(2, 3)

関数型 #

関数型
type BinaryOp func(int, int) int

func apply(op BinaryOp, a, b int) int {
	return op(a, b)
}

apply(add, 2, 3)   // 5

関数シグネチャを型エイリアスにします。コールバックを受け取るAPIでよく登場する形です。

クロージャ — 関数が環境を持ち歩く #

JSのクロージャと同じ概念。Goでもそのまま。

クロージャでカウンター
func makeCounter() func() int {
	count := 0
	return func() int {
		count++
		return count
	}
}

counter := makeCounter()
fmt.Println(counter())   // 1
fmt.Println(counter())   // 2
fmt.Println(counter())   // 3

返された無名関数が count をキャプチャして生き残ります。JSと同じ動作です。

メソッド — 次の記事のプレビュー #

関数が特定の型に紐づいていればメソッドと呼びます。

メソッドプレビュー
type Counter struct {
	count int
}

func (c *Counter) Increment() {
	c.count++
}

c := &Counter{}
c.Increment()
c.Increment()
fmt.Println(c.count)   // 2

func (c *Counter) Increment()(c *Counter) 部分がレシーバ。この関数は *Counter のメソッドです。#6 構造体とメソッド で詳しく。

よく使うパターン #

1) エラーを上に流す #

エラー伝播
func processFile(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("ファイルを開くのに失敗: %w", err)
	}
	defer file.Close()

	if err := parse(file); err != nil {
		return fmt.Errorf("パース失敗: %w", err)
	}
	return nil
}

%w verbが核心 — 元のエラーをwrappingします。呼び出し側が errors.Is/errors.As で中のエラーまで検査できるようになります。

2) エラーと結果を一度に返す #

結果 + エラー
func parseUser(input string) (*User, error) {
	parts := strings.Split(input, ":")
	if len(parts) != 2 {
		return nil, fmt.Errorf("不正な形式: %q", input)
	}
	return &User{Name: parts[0], ID: parts[1]}, nil
}

成功なら (結果, nil)、失敗なら (zero値, エラー) が標準です。

3) 関数の分解 — 小さく切る #

Goコードでは一般的に、関数を小さく切り、各関数が一つのことをするようにします。early return + エラーwrappingで流れがすっきりします。

まとめ #

今回の記事で整理した内容:

  • func 名前(仮引数) 戻り値型 { }
  • 同じ型はまとめて書く、名前付き戻り値も可
  • 多値返却 — Goのシグネチャ
  • error はインターフェース — Error() string メソッドだけあればOK
  • if err != nil { return err } が標準パターン
  • errors.New / fmt.Errorf でエラー生成、%w でwrapping
  • defer で後始末 — LIFO順序、引数は即時評価
  • 可変長引数 ...、スライス展開 nums...
  • 関数も第一級値、クロージャ動作
  • メソッドは #6

次の記事(#5 コレクション)ではGoの3つのコレクション — 配列、スライス、マップ — の使い方とよく出会う罠を扱います。

X