【Go】パスパラメータを使用するときの注意点やエラーハンドリングなど

# はじめに
前記事からの続きで、パスパラメータのidでcustomerの個別データを取得するAPIを作成しました。
エラーハンドリングなど、以上のケースにおけるAPI作成にあたっての注意点をメモとしてまとめました。
Go初心者の方の参考になれば幸いです。

https://qiita.com/suzuki0430/items/6e9d070bb529f4d91500

# 数値以外のパスパラメータをはじく
idとして渡すパスパラメータが数値のみとなるように、正規表現を使用します。

```go:app.go
router.HandleFunc("/customers/{customer_id:[0-9]+}", ch.getCustomer).Methods(http.MethodGet)
```

試しにパラメータに数値以外`ddd`を入力してみると、`404 page not found`が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/2ef1821e-19ef-87c8-aeef-841f42aebfd0.png" width="600">

# JSONオブジェクトを返す
Handlerとして、`getCustomer`を新しく作成します。
取得したパラメータを元に`ch.service.GetCustomer(id)`でデータを取得し、`err`の有無に関わらずに`writeResponse()`を返すようにします。
`writeResponse()`では、Content-Typeの設定、ヘッダーへのステータスコードの付加、json形式へのデータのエンコードを行います。

```go:customerHandlers.go
func (ch *CustomerHandlers) getCustomer(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["customer_id"]

	customer, err := ch.service.GetCustomer(id)
	if err != nil {
		writeResponse(w, err.Code, err.AsMessage())
	} else {
		writeResponse(w, http.StatusOK, customer)
	}
}

func writeResponse(w http.ResponseWriter, code int, data interface{}) {
	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(code)
	if err := json.NewEncoder(w).Encode(data); err != nil {
		panic(err)
	}
}
```

# 指定したidのデータがないときに404を返す
前項の`customer, err := ch.service.GetCustomer(id)`で`err`に渡す中身を考えます。
リポジトリパターンのため、処理内容はServiceではなくRepositoryに記載されます。

なお、`(*domain.Customer, *errs.AppError)`のように`GetCustomer()`の返り値がポインタ型となっていますが、データがなかった場合にnilを返すためにこのようにしています。
初期値をnilにするときなどにもポインタ型は使われます。

```go:customerService.go
func (s DefaultCustomerService) GetCustomer(id string) (*domain.Customer, *errs.AppError) {
	return s.repo.ById(id)
}
```

話を戻しますが、`customerRepositoryDb`のメソッド`ById()`を以下のように記述します。

```go:domain/customerRepositoryDb.go
func (d CustomerRepositoryDb) ById(id string) (*Customer, *errs.AppError) {
	customerSql := "select customer_id, name, city, zipcode, date_of_birth, status from customers where customer_id = ?"

	row := d.client.QueryRow(customerSql, id)
	var c Customer
	err := row.Scan(&c.Id, &c.Name, &c.City, &c.Zipcode, &c.DateofBirth, &c.Status)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, errs.NewNotFoundError("customer not found")
		} else {
			log.Println("Error while scanning customer " + err.Error())
			return nil, errs.NewUnexpectedError("unexpected database error")
		}
	}
	return &c, nil
}
```

`row.Scan()`で`err`が返ってきたときのエラーハンドリングで、さらに`if err == sql.ErrNoRows`でデータが空だったときの分岐を行います。
`errs.NewNotFoundError("customer not found")`のようなエラーを返していますが、この中身はMessageとCodeを含んだAppErrorという構造体となります。

```go:errs/errors.go
package errs

import "net/http"

type AppError struct {
	Code int	`json:",omitempty"`
	Message string	`json:"message"`
}

func (e AppError) AsMessage() *AppError {
	return &AppError{
		Message: e.Message,
	}
}

func NewNotFoundError(message string) *AppError {
	return &AppError {
		Message: message,
		Code: http.StatusNotFound, // 404
	}
}
```

JSONにCodeを加えない場合に、`AsMessage()`というメソッドも用意しています。

# 想定外のエラーの際に500をメッセージとあわせて返す
`ById()`のエラーハンドリングで、データが空じゃない場合のエラー(想定外のエラー)として、`errs.NewUnexpectedError("unexpected database error")`を返します。

こちらについては、500のCodeとMessageを含んだ構造体を返すようにします。

```go:errs/errors.go
func NewUnexpectedError(message string) *AppError {
	return &AppError {
		Message: message,
		Code: http.StatusInternalServerError,
	}
}
```

# 参考資料

https://www.udemy.com/share/103O5K3@583FXuJyw2txuRwP7nOpOhS3Z_E7NGTSu5REvPbqC4O7LXEbME25xJrvSQigFK9B/

https://pon3-blog.com/?p=137