Go基礎 #4 関数、多値返却、error型
#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
}同じ型が連続するなら最後に一度だけ書きます。
戻り値なし #
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)
}q、r が関数開始時に自動で宣言され、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はインターフェース #
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 の方が一般的です。
return 0, fmt.Errorf("0で割ろうとしている a=%d", a)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接続を閉じる)にほぼ標準で使う道具。
func readFile() {
file, err := os.Open("data.txt")
if err != nil {
return
}
defer file.Close() // 関数終了時に自動で呼び出される
// ... ファイルを使用
}defer はその行をすぐ実行するのではなく、関数がreturnする直前に実行されるよう予約します。どんな経路(正常return、panicなど)で関数を抜けても呼び出されます。
複数のdeferは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
// 関数終了 — 出力: 10fmt.Println(i) のiがdefer時点で評価され10で固定されます。その後にiが変わっても影響ありません。
これを避けたければクロージャで包みます。
i := 10
defer func() { fmt.Println(i) }() // 実行時点で i を評価
i = 20
// 出力: 20可変長引数 — ...
#
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() // 0nums ...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) // 2func (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メソッドだけあればOKif err != nil { return err }が標準パターンerrors.New/fmt.Errorfでエラー生成、%wでwrappingdeferで後始末 — LIFO順序、引数は即時評価- 可変長引数
...、スライス展開nums... - 関数も第一級値、クロージャ動作
- メソッドは #6 で
次の記事(#5 コレクション)ではGoの3つのコレクション — 配列、スライス、マップ — の使い方とよく出会う罠を扱います。