go で AggregationException(.NET)的なことをする

おはようございます。皆さん、せい・・聖夜は如何お過ごしの予定でしょうか。

さてさて、Goのエラー処理についてはやいやい言われていますね。 エラートラップ忘れだの、エラーをどういうふうに伝播するべきか、といった記事がたくさんあります。侃々諤々といった感じで良いですね。

でも、意外と「ループの中で、エラーが発生しても無視して次に進めるが、発生したエラーを最後にまとめて回収する」みたいな話がなかったので、書いてみようと思います。 無いのは需要が無い(または自明過ぎて誰も書かない)からだ!と言われないうちに書ききってしまいましょう。

https://github.com/kyoh86/qiita

package util

import (
	"encoding/json"
	"errors"
	"fmt"
)

// Errors : 複数のエラーを一括するエラー
type Errors []error

// Push : エラーを追加する
func (a *Errors) Push(item error) {
	if item == nil {
		return
	}
	if arr, ok := item.(Errors); ok && arr != nil {
		for _, child := range arr {
			a.Push(child)
		}
	} else {
		*a = append(*a, item)
	}
}

// IsEmpty : エラーが空かどうか取得する
func (a *Errors) IsEmpty() bool {
	return a == nil || len(*a) == 0
}

// Error : エラー出力を得る
func (a Errors) Error() string {
	array := make([]string, len(a))
	for i, item := range a {
		array[i] = item.Error()
	}
	buf, _ := json.MarshalIndent(array, "", "  ")
	return string(buf)
}

// Err : 空の場合は nil 、それ以外の場合は自身を返す
func (a Errors) Err() error {
	if a.IsEmpty() {
		return nil
	}
	return a
}

キモはこれだけです。 使い方としては、

package main

import (
	"errors"
	"fmt"
	"strings"

	"github.com/kyoh86/qiita/util"
)

func main() {
	nofbzz, err := SekaiNoNabeatsu(100)
	fmt.Println("まともに言えた数:")
	for _, n := range nofbzz {
		fmt.Println(n)
	}
	fmt.Println("アホまたは犬:")
	fmt.Println(err)
}

// SekaiNoNabeatsu は、3の倍数だけアホになり、5の倍数だけ犬っぽくなります
func SekaiNoNabeatsu(num int) ([]int, error) {
	var ret []int
	var errs = new(util.Errors)

	for i := 0; i < num; i++ {
		fizz := i % 3
		buzz := i % 5
		switch {
		case fizz == 0 && buzz == 0:
			errs.Push(errors.New("くぅ〜ん" + strings.Repeat("!", i/5/3)))
		case fizz == 0 && buzz != 0:
			errs.Push(errors.New("さぁ〜ん" + strings.Repeat("!", i/3)))
		case fizz != 0 && buzz == 0:
			errs.Push(errors.New("わん" + strings.Repeat("!", i/5)))
		default:
			ret = append(ret, i)
		}
	}
	return ret, errs.Err()
}

みたいな感じ。ナベアツの仕様については仕様通りと言い張ります。

最後の「errs.Err()」が結構ポイントです。 ポインタなんだから、var errs *util.Errorsとしておいて、最初のエラーでnew(util.Errors)して、そのまま返せばいいじゃん! とか横着すると、ハマります。 【参考】理由2. nilのインターフェースは必ずしもnilではない

ところで、errs.Pushするのに、毎回errors.Newするのめんどくさいですよね。 ここに入る処理って、下流のerrをそのまま突っ込む(errs.Push(err))か、errors.Newするか、fmt.Errorfするかくらいですよね。 そんでもって、ナベアツのエラーをアホなのか犬なのか、アホな犬なのかで並び替えたいですよね。

なのでUtility追加しましょう。

// SPush : 文字列でエラーを作成し、追加する
func (a *Errors) SPush(text string) {
	a.Push(errors.New(text))
}

// Pushf : 書式付き文字列でエラーを作成し、追加する
func (a *Errors) Pushf(format string, p ...interface{}) {
	a.Push(fmt.Errorf(format, p...))
}

func (a Errors) Len() int      { return len(a) }
func (a Errors) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a Errors) Less(i, j int) bool {
	return a[i].Error() < a[j].Error()
}

色々やってきましたが、セカイのナベアツの面白さは、数字を下から読み上げていって、唐突にアホになったり犬になったりするから面白いんだなぁ、と冷静になりました。 最後にこのプログラムの出力を貼って厳かな気持ちで終わりたいと思います。

まともに言えた数:
1
2
4
7
8
11
13
14
16
17
19
22
23
26
28
29
31
32
34
37
38
41
43
44
46
47
49
52
53
56
58
59
61
62
64
67
68
71
73
74
76
77
79
82
83
86
88
89
91
92
94
97
98
アホまたは犬:
[
  "くぅ〜ん",
  "さぁ〜ん!",
  "わん!",
  "さぁ〜ん!!",
  "さぁ〜ん!!!",
  "わん!!",
  "さぁ〜ん!!!!",
  "くぅ〜ん!",
  "さぁ〜ん!!!!!!",
  "わん!!!!",
  "さぁ〜ん!!!!!!!",
  "さぁ〜ん!!!!!!!!",
  "わん!!!!!",
  "さぁ〜ん!!!!!!!!!",
  "くぅ〜ん!!",
  "さぁ〜ん!!!!!!!!!!!",
  "わん!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!",
  "わん!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!",
  "くぅ〜ん!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!",
  "わん!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!",
  "わん!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!",
  "くぅ〜ん!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!",
  "わん!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!",
  "わん!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!",
  "くぅ〜ん!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!!!",
  "わん!!!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
  "わん!!!!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
  "くぅ〜ん!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
  "わん!!!!!!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
  "さぁ〜ん!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
]

お、おう。。。