AWS SDK for Goのエラーハンドリング

# はじめに
Golang のエラーハンドリングは、基本的に error 型に詰め込まれた情報を扱う。
nil 値以外かどうかを判別するのは大したことがないが、error 型が interface があるがゆえに自由度が高く、真面目にハンドリングしようとするとちょっと厄介である。

- 参考: 【Qiita】[Golangのエラーハンドリングの基本](https://qiita.com/immrshc/items/13199f420ebaf0f0c37c)

今回は、 AWS SDK for Go でどのようにエラーハンドリングをしたら良いか、DynamoDB を例に取って確認する。
なお、基本的に[APIリファレンスにハンドリング付きの例](https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/#pkg-examples)があるので、これを見れば済む話ではある。

# PutItem() のような単純なエラー
AWS SDK for Go のエラー情報は、基本的に[型アサーション(Type assertions)](https://go-tour-jp.appspot.com/methods/15)で取得する。

PutItem() の場合は、以下のような感じだ。
DynamoDBのテーブルには、あらかじめ id:00001 のユーザを入れておき、ConditionExpression でエラーになるようにしておく。

```Golang
	if _, err := svc.PutItem(&dynamodb.PutItemInput{
		TableName: aws.String(ddbTableName),
		Item: map[string]*dynamodb.AttributeValue{
			"id": {
				S: aws.String("00001"),
			},
			"name": {
				S: aws.String("Taro"),
			},
			"age": {
				S: aws.String("36"),
			},
		},
		ConditionExpression: aws.String("attribute_not_exists(id)"),
	}); err != nil {
		log.Println(reflect.TypeOf(err))
		if awserr, ok := err.(awserr.Error); ok {
			log.Println("awserr.Code(): " + awserr.Code())
			log.Println("awserr.Message(): " + awserr.Message())
		}
	}
```

これを実行すると、以下のような出力となる。

```
2021/04/29 14:31:07 *dynamodb.ConditionalCheckFailedException
2021/04/29 14:31:07 awserr.Code(): ConditionalCheckFailedException
2021/04/29 14:31:07 awserr.Message(): The conditional request failed
```

interface で dynamodb.ConditionalCheckFailedException が渡されてくるようだ。
```awserr.Code()``` で分岐を作れば、エラーハンドリングが可能になる。

PutItem() は構造が単純だが、では TransactWriteItems() のように複数のアイテムの情報を扱うような複雑な構造の場合はどうだろう？

# TransactWriteItems() のような複雑なエラー
まずは PutItem() のように書いてみよう。
今度は、1件目の id:00002 は正常に処理が行えて、2件目の id: 00001 で ConditionExpression となってエラーになるようにしておく。


```Golang
	if _, err := svc.TransactWriteItems(&dynamodb.TransactWriteItemsInput{
		TransactItems: []*dynamodb.TransactWriteItem{
			{
				Put: &dynamodb.Put{
					TableName: aws.String(ddbTableName),
					Item: map[string]*dynamodb.AttributeValue{
						"id": {
							S: aws.String("00002"),
						},
						"name": {
							S: aws.String("Jiro"),
						},
						"age": {
							S: aws.String("25"),
						},
					},
				},
			},
			{
				Put: &dynamodb.Put{
					TableName: aws.String(ddbTableName),
					Item: map[string]*dynamodb.AttributeValue{
						"id": {
							S: aws.String("00001"),
						},
						"name": {
							S: aws.String("Taro"),
						},
						"age": {
							S: aws.String("36"),
						},
					},
					ConditionExpression: aws.String("attribute_not_exists(id)"),
				},
			},
		},
	}); err != nil {
		log.Println(reflect.TypeOf(err))
		if awserr, ok := err.(awserr.Error); ok {
			log.Println("awserr.Code(): " + awserr.Code())
			log.Println("awserr.Message(): " + awserr.Message())
		}
	}
```

これを実行すると、

```
2021/04/29 14:37:10 *dynamodb.TransactionCanceledException
2021/04/29 14:37:10 awserr.Code(): TransactionCanceledException
2021/04/29 14:37:10 awserr.Message(): Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]
```

という出力になる。
これでは、```TransactionCanceledException``` の原因を拾って条件分岐させることができない。

```TransactionCanceledException``` は、[APIリファレンス](https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/#TransactionCanceledException)によると、```CancellationReasons``` というエラー理由の配列を持っているようだ。
なので、リファレンスで ```CancellationReasons``` の構造体を追いかけ、さらに以下のようにこの配列の中身を見てみる。
```TransactionCanceledException``` であることを確実に判別したうえでハンドリングする必要があるため、```err.(type)``` で switch を作り、何の型が帰ってきているかを特定しよう。

```Golang
	}); err != nil {
		log.Println(reflect.TypeOf(err))
		if awserr, ok := err.(awserr.Error); ok {
			log.Println("awserr.Code(): " + awserr.Code())
			log.Println("awserr.Message(): " + awserr.Message())
		}
		switch t := err.(type) {
		case *dynamodb.TransactionCanceledException:
			log.Println(*t.CancellationReasons[0].Code)
			log.Println(*t.CancellationReasons[1].Code)
		default:
			log.Printf("failed to write items: %v", err)
		}
	}
```

これで、

```
2021/04/29 14:46:18 awserr.Code(): TransactionCanceledException
2021/04/29 14:46:18 awserr.Message(): Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]
2021/04/29 14:46:18 t.CancellationReasons[0].Code: None
2021/04/29 14:46:18 t.CancellationReasons[1].Code: ConditionalCheckFailed
```

という出力が得られ、TransactionCanceledException の発生理由の条件分岐が作れるようになった！

基本はこのような感じなので、他の API についても、この要領でハンドリングをすることが可能だろう。