こんにちわ。んだです。
今回も『実用 Go言語』の読書録です。
インターフェースの設計のポイント
まずは、Goのインターフェース設計の指針についての記述があったので紹介します。
Goでは「何でもインターフェースにして柔軟性を保つ」「いつでも実装を切り替えられるようにする」という設計はされていませんでした。(...中略)さまざまな事情はあると思いますが、過剰に複雑な実装をするくらいならインターフェースを使わずにコードを書くという指針で十分です。あらかじめ、テストを考えてモック化できるようにインターフェースにしておく、という考えをしているGoユーザーは少数派です。
この辺は、Go特有というわけではなく、他の言語でも同じことが言えますね。
Example
では、Exampleを『Writing A Compiler In Go』から見てみましょう。
interfaceの定義
interfaceの定義は、こんな感じです。
type Node interface { TokenLiteral() string String() string } type Statement interface { Node statementNode() } type Expression interface { Node expressionNode() }
interfaceの具象
interfaceを満たすには、インターフェースで定義しているメソッドを実装すればOKです。 以下の実装は、Nodeインターフェースを実装しているということになります。
func (p *Program) TokenLiteral() string { if len(p.Statements) > 0 { return p.Statements[0].TokenLiteral() } else { return "" } } func (p *Program) String() string { var out bytes.Buffer for _, s := range p.Statements { out.WriteString(s.String()) } return out.String() }
エラーハンドリング
続いてはエラーハンドリング
エラーはただの値。
まず、おさえておくことはGoにおいては、エラーはただの値であること。PHPでは、try-catchで捕縛することはできるが、Goでは例外処理はない。
よって、
if err != nil { // エラー発生時の挙動 }
という書き方をする。
個人的には明示的にエラー発生時の挙動をかけるので、いいなと思う点ではある。 が、冗長な表現になってしまうという点も否めない。
ここらへんを解消するは、いろんな小技があるらしい。
独自のエラー型
で、Goでは、buildinでerrorインターフェースが定義されている。
よって、こんな感じに独自のエラーを簡単に定義することが可能です。
type HTTPError struct { StatusCode int URL string } func (he *HTTPError) Error() string { return fmt.Sprintf("http status code = %d, url = %s", he.StatusCode, he.URL) } func ReadContents(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, &HTTPError{ StatusCode: resp.StatusCode, URL: url, } } return io.ReadAll(resp.Body) }
エラーのラップ
ここからは、以下の書籍をもとに書いていきます。
エラーラッピングとは?
まず、エラーをラップするというのは、どういうことか?なんですが、
イメージ的には、こんな感じです。(本書を参考に作成。
そして、どんなときにラップするか?ですが、
- エラーに文脈情報を追加する
- エラーを特定エラーとしてマークする
の2つです。
Go1.13から導入された%w
Go1.13より前のバージョンではエラーをラップしたい場合には、前述のHTTPErrorのように独自のエラーを開発者が定義してラップするしかなかった。
が、Go1.13から導入された%wによって
if err != nil { return fmt.Errorf("bar failed : %w, err) }
という書き方が可能になり、独自のエラーを定義しなくてもエラーに文脈情報を追加することができるようになった。
エラー型の検査
続いては、errorAs()について書いていく。
まずは、次のコードを見てみましょう。
type transientError struct { err error } func (t transientError) Error() string { return fmt.Sprintf("transient error: %v", t.err) } func GetTransactionAmountHandler(w http.ResponseWriter, r *http.Request) { transactionID := r.URL.Query().Get("transaction") amount, err := getTransactionAmount(transactionID) if err != nil { switch err := err.(type) { case transientError: http.Error(w, err.Error(), http.StatusServiceUnavailable) default: http.Error(w, err.Error(), http.StatusBadRequest) } } } func getTransactionAmount(transactionID string) (float32, error) { if len(transactionID) != 5 { return 0, fmt.Errorf("id is invalid: %s", transactionID) } amount, err := getTransactionAmountFromDB(transactionID) if err != nil { return 0, transientError{err: err} } return amount, nil } func getTransactionAmountFromDB(transactionID string) (float32, error) { var amount float32 err := db.QueryRow("SELECT amount FROM transactions WHERE id = $1", transactionID).Scan(&amount) if err != nil { return 0, transientError{err: err} } return amount, nil }
GetTransactionAmountHandler
で、エラーの型をみて、transientErrorであれば503、それ以外は400を返している。
この状態であれば、特に問題はないが、getTransactionAmount
を以下のようにリファクタリングした場合を考えてみましょう。
func getTransactionAmount(transactionID string) (float32, error) { if len(transactionID) != 5 { return 0, fmt.Errorf("id is invalid: %s", transactionID) } amount, err := getTransactionAmountFromDB(transactionID) if err != nil { return 0, fmt.Errorf("failed to get transaction %s: %w", transactionID, err) } return amount, nil }
return 0, fmt.Errorf("failed to get transaction %s: %w", transactionID, err)
で、transientErrorをラップして返しています。
しかし、このように変更すると、case transientError:
を通らなくなってしまいます。
func GetTransactionAmountHandler(w http.ResponseWriter, r *http.Request) { transactionID := r.URL.Query().Get("transaction") amount, err := getTransactionAmount(transactionID) if err != nil { switch err := err.(type) { case transientError: http.Error(w, err.Error(), http.StatusServiceUnavailable) default: http.Error(w, err.Error(), http.StatusBadRequest) } }
そこで、登場するのが、errors.As() errors.As()は、ラップされたエラーの型をチェックしてくれます。
errors.As()で書き直したGetTransactionAmountHandlerがこちら。
func GetTransactionAmountHandler(w http.ResponseWriter, r *http.Request) (float32, error) { transactionID := r.URL.Query().Get("transaction") amount, err := getTransactionAmount(transactionID) if err != nil { if errors.As(err, &transientError{}) { http.Error(w, err.Error(), http.StatusServiceUnavailable) return 0, err } http.Error(w, err.Error(), http.StatusBadRequest) return 0, err } return amount, nil }
errors.As()を使って、ラップされたエラーの型をチェックしています。エラーの型ではなく、値を確認する場合には、Is()を使ってあげればOK
まとめ
インターフェースとエラーハンドリングについて、見てきました。 エラーハンドリングには、まだいろいろと小技とか気を付けるべきところがありそうなので、これからぼちぼち理解していきたいと思います。
僕から以上。あったかくして寝ろよ。