Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@tokubass
Last active March 23, 2017 09:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tokubass/1fcc94d05a88a8912d0eda35ce7dc623 to your computer and use it in GitHub Desktop.
Save tokubass/1fcc94d05a88a8912d0eda35ce7dc623 to your computer and use it in GitHub Desktop.

7.10 typ assertionによる分析的エラー

osパッケージのfileオペレーションによって返却されたエラーの組について考える。 I/Oはいくつかの理由で失敗することがあるが、3つの種類の障害を頻繁に処理する必要がある。

  • file already exists (create)
  • file not found (read)
  • permission denied

os packageはこれらをerror 値で示される失敗を分類するhelper関数を提供します。

ナイーブな実装は、エラーメッセージに特定の文字列が含まれているかチェックする。 しかし他のプラットフォームごとに異なるメッセージになるので、ロバストではない。 また、同じエラーが異なるメッセージで報告されるかもしれない。 エラーメッセージのチェックはテスト中に関数の振る舞いを保証するには良いが、プロダクションでは不適

もっと信頼できるアプローチは、エラー値の構造体を表す専用の型を使うこと。 os packageにはPathError型がある file pathのオペレーションによって起こる失敗を表現している。Open,Delete

2つのfile pathのオペレーションで起こる失敗を表すLinkError型もある。 SymlinkRename

type PathError struct {
    Op    string
    Path  string
    Error Error
}

func (e *PathError) String() string {
    return e.Op + " " + e.Path + ": " + e.Error.String()
}
type LinkError struct {
    Op    string
    Old   string
    New   string
    Error Error
}

ほとんどのクライアントはPathErrorを知らず、Errorメソッドを呼び出すことですべてのエラーを一様に処理します。 PathErrorのErrorメソッドはフィールドを連結するだけでメッセージを形成しますが、PathErrorの構造はエラーの基礎となるコンポーネントを保持します。 ある種類の障害と他の種類の障害を区別する必要があるクライアントは、タイプアサーションを使用してエラーの特定の種類を検出できます。 特定の型は単純な文字列よりも詳細を提供します。

たとえば、以下に示すIsNotExistは、エラーがsyscall.ENOENTまたはos.ErrNotExistに等しいかどうかを報告します。 または、*PathErrorの基礎となるエラーがこれらの2つのいずれかになります。

func IsNotExist( err error) bool {
  if pe, ok := err.(* PathError); ok {
    err = pe.Err
  }
  return err = = syscall.ENOENT | | err = = ErrNotExist
}

もちろん、PathErrorの構造体は、エラーメッセージが大きな文字列に結合されていると、たとえばfmt.Errorfの呼び出しによって失われます。 エラーの識別は通常、エラーが発呼者に伝播される前に、失敗した操作の直後に行われなければならない。

7.11 Querying Behaviors with Interface Type Assertions

以下のロジックは、 "Content-type:text/html"などのHTTPヘッダーフィールドの作成を担当するnet/http Webサーバーの部分と似ています。 io.Writer wはHTTP応答を表します。 それに書き込まれたバイトは最終的に誰かのWebブラウザに送られます。

func writeHeader( w io.Writer, contentType string) error {
  if _, err := w.Write([] byte(" Content-Type: ")); err != nil {
   return err
  }
  if _, err := w.Write([] byte( contentType)); err != nil {
    return err
  }
  // ...
}

Writeメソッドはバイトスライスを必要とし、書き込みたい値は文字列であるため、[]バイト(...)変換が必要です。 この変換はメモリを割り当ててコピーを作成しますが、コピーはほとんど直後に破棄されます。 これがWebサーバーの中核部分であると推測し、プロファイリングがこのメモリー割り当てがそれを遅くしていることを明らかにしてみましょう。 ここでメモリを割り当てるのを避けることはできますか?

io.Writerインターフェイスは、wが保持する具体的な型に関する1つの事実、つまりバイトが書き込まれることのみを示しています。 net/httpパッケージのカーテンを見ると、このプログラムで保持されている動的な型には、文字列を効率的に書き込むことができるWriteStringメソッドがあり、一時的なコピーを割り当てる必要はありません。

(これは暗闇でのショットのように見えるかもしれませんが、io.Writerには*bytes.Buffer*os.File*bufio.WriterなどのWriteStringメソッドもあります) 任意のio.Writer wにもWriteStringメソッドがあるとは想定できません。 しかし、このメソッドだけを持つ新しいインターフェイスを定義し、wの動的型がこの新しいインターフェイスを満たすかどうかをテストするために型アサーションを使用することができます。

// writeString writes s to w.
// If w has a WriteString method, it is invoked instead of w.Write.

func writeString( w io.Writer, s string) (n int, err error) {
  type stringWriter interface {
    WriteString( string) (n int, err error)
  }
  if sw, ok := w.( stringWriter); ok {
    return sw.WriteString(s) // avoid a copy 
  }
  return w.Write([] byte( s)) // allocate temporary copy
}
func writeHeader( w io.Writer, contentType string) error {
  if _, err := writeString( w, "Content-Type: "); err != nil {
    return err
  }
  if _, err := writeString( w, contentType); err != nil {
    return err
  }
  // ...
}

繰り返しを避けるため、チェックをユーティリティ関数のwriteStringに移しましたが、標準ライブラリではio.WriteStringとして提供するのが非常に便利です。 これは、文字列をio.Writerに書き込むための推奨される方法です。 この例で興味深いのは、WriteStringメソッドを定義し、必要な動作を指定する標準インターフェイスがないことです。 さらに、具体的な型がstringWriterインタフェースを満たすかどうかは、そのメソッドによってのみ決定され、インタフェース型との宣言された関係によっては決定されません。 これが意味することは、上記のテクニックは、タイプが下のインターフェースを満たす場合、WriteStringはWrite([] byte(s))と同じ効果を持たなければならないという前提に依存しているということです。

interface {
 io.Writer
 WriteString( s string) (n int, err error)
}

io.WriteStringはその前提を文書化していますが、それを呼び出す関数はほとんどありませんが、それらも同じ前提を立てていると書かれています。特定のタイプのメソッドを定義することは、特定のビヘイビア契約の暗黙的な同意とみなされます。 Go初心者、特に強く型付けされた言語のバックグラウンドを持つ人は、この明白な意図が不安定であることがわかるかもしれませんが、実際にはめったに問題にはなりません。空のインタフェース interface{}を除いて、意図しない偶然によってインタフェース・タイプがほとんど満たされません。

上記のwriteString関数は、タイプアサーションを使用して、一般的なインターフェイスタイプの値がより特定のインターフェイスタイプを満たしているかどうかを確認し、そうであれば、特定のインターフェイスの動作を使用します。照会されたインターフェースがio.ReadWriterstringWriterのようなユーザー定義の標準であるかどうかにかかわらず、このテクニックは有効です。

また、fmt.Fprintfは、エラーまたはfmt.Stringerを満たす値を他のすべての値と区別します。 fmt.Fprintfには、単一のオペランドを文字列に変換するステップがあります。

package fmt

func formatOneValue( x interface{}) string {
  if err, ok := x.(error); ok {
    return err.Error()
  }
  if str, ok := x.(Stringer); ok {
    return str.String()
  } 
  // ... all other types... 
}

xが2つのインターフェースのいずれかを満たす場合、xは値のフォーマットを決定します。 そうでない場合、デフォルトの場合は、リフレクションを使用して他のすべての型を多少なりとも一様に扱います。 第12章でその方法を見ていきます。これは、Stringメソッドを持つ型がfmt.Stringerの動作規約を満たし、印刷に適した文字列を返すことを前提にしています。

7.13 Type Switches

インターフェイスは2つの異なるスタイルで使用されます。最初のスタイルでは、io.Reader、io.Writer、fmt.Stringer、sort.Interface、http.Handler、およびerrorのように、インターフェイスのメソッドはインターフェイスを満たす具体的な型の類似点を表しますが、これらの具体的な型の本質的な操作。コンクリート型ではなくメソッドに重点を置いています。

 第2のスタイルは、インタフェース値が様々な具体的な型の値を保持する能力を利用し、インタフェースをそれらの型の集合体とみなします。タイプアサーションは、これらのタイプを動的に区別し、それぞれのケースを異なる方法で扱うために使用されます。このスタイルでは、インタフェースのメソッド(実際にはそれがある場合)ではなく、インタフェースを満たす具体的な型に重点が置かれ、情報の隠蔽はありません。この方法で使用されるインタフェースについては、区別された共用体として説明します。

 オブジェクト指向プログラミングに精通している場合は、これらの2つのスタイルをサブタイプの多型と特殊な多型と認識しますが、それらの用語を覚える必要はありません。この章の残りの部分では、2番目のスタイルの例を示します。

 他の言語のようなSQLデータベースを照会するためのGoのAPIは、クエリの固定部分を可変部分からきれいに分離することができます。クライアントの例は次のようになります。

import "database/ sql"

func listTracks( db sql.DB, artist string, minYear, maxYear int) {
  result, err := db.Exec(
    "SELECT * FROM tracks WHERE artist = ? AND ? < = year AND year < = ?",
     artist, minYear, maxYear)
   // ...
}

Execメソッドはそれぞれの '?' ブール型、数値型、文字列型、またはnil型の対応する引数の値を示すSQLリテラルを含むクエリ文字列内に指定します。 このようにクエリを作成すると、SQLインジェクション攻撃が回避され、不正な入力データの引用を悪用してクエリを制御することができます。 Exec内では、各引数の値をリテラルのSQL表記に変換する次のような関数があります。

func sqlQuote( x interface{}) string {
 if x = = nil {
  return "NULL"
 } else if _, ok := x.( int); ok {
  return fmt.Sprintf("% d", x)
 } else if _, ok := x.( uint); ok {
  return fmt.Sprintf("% d", x)
 } else if b, ok := x.( bool); ok {
  if b {
   return "TRUE"
  }
  return "FALSE"
 } else if s, ok := x.(string); ok {
  return sqlQuoteString(s) // (not shown)
 } else {
  panic( fmt.Sprintf(" unexpected type %T: %v", x, x))
 }
}

switch文は、一連の値の等価性テストを実行するif-elseチェーンを単純化します。 類似の型switch文は、型assertionのif-else連鎖を単純化します。

最も単純な形式では、タイプスイッチは、オペランドがx.(type)である通常のswitch文のように見えます。これは文字通りキーワードの型であり、それぞれの場合には1つ以上の型があります。 タイプスイッチは、インタフェース値の動的型に基づいて多方向分岐を可能にする。 x == nilの場合、nil caseは一致し、他のcaseがない場合はデフォルトのcaseが一致します。 sqlQuoteのタイプスイッチには次のようなケースがあります。

switch x.(type) {
 case nil: // ...
 case int, uint: // ...
 case bool: // ...
 case string: // ...
 default: // ... 
}

通常のswitch文(§1.8)と同様に、caseは順番に考慮され、一致が見つかると、caseの本体が実行されます。 1つ以上のケースタイプがインタフェースである場合、ケース順序が重要になります。これは、2つのケースが一致する可能性があるためです。 defaultケースの相対的な位置は、重要ではありません。 fallthroughは許されません。

元の関数では、boolおよびstringの場合のロジックは、型アサーションによって抽出された値にアクセスする必要があることに注意してください。 これは典型的なので、type switch文には、抽出された値をそれぞれの場合に新しい変数にバインドする拡張形式があります。

switch x := x.(type) {/* ... */ }

ここでは、新しい変数 xも呼び出しました。 型アサーションと同様に、変数名の再利用は一般的です。 switchステートメントのように、タイプスイッチは暗黙的に字句ブロックを作成するので、新しい変数 xの宣言は外側ブロックの変数 xと競合しません。 各 `case 'は暗黙的に別個の字句ブロックを作成する。

sqlQuoteをタイプスイッチの拡張形式を使用して書き直すと、それはかなり明確になります:

func sqlQuote( x interface{}) string {
 switch x := x.(type) {
 case nil:
  return "NULL"
 case int, uint:
  return fmt.Sprintf("% d", x) // x has type interface{} here.
 case bool:
  if x {
   return "TRUE" 
  }
  return "FALSE"
 case string:
  return sqlQuoteString( x) // (not shown)
 default:
  panic( fmt.Sprintf(" unexpected type %T: %v", x, x))
 }
}

このバージョンでは、各単一型ケースのブロック内で、変数 xはケースと同じ型を持ちます。 例えば、 x boolの中に bool型を持ち、stringの中に stringを持っています。 それ以外の場合、 x switchオペランドの(インタフェース)タイプを持ちます。このオペランドはこの例では interface {}です。 int uintのような複数のケースで同じアクションが必要な場合は、タイプスイッチを使用すると簡単に組み合わせることができます。

  sqlQuoteは任意の型の引数を受け付けますが、引数の型が型切り替えのケースの1つと一致する場合にのみ、関数は完了まで実行されます。 それ以外の場合は、「予期しないタイプ」のメッセージが表示されます。 xの型はinterface {}ですが、 intuint boolstring nilの区別された和集合とみなします。

7.14 Example: Token-Based XML Decoding

4.5節では、 encoding / jsonパッケージから MarshalUnmarshal関数を使ってJSON文書をGoデータ構造体にデコードする方法を示しました。 encoding / xmlパッケージは、同様のAPIを提供します。 このアプローチは、ドキュメントツリーの表現を作成したいときに便利ですが、多くのプログラムでは不要です。 encoding / xmlパッケージは、XMLを解読するためのlower-level token-based APIも提供します。 トークンベースのスタイルでは、パーサーは入力を消費し、 StartElement EndElementCharData Commentの4種類のトークンのストリームを生成します。それぞれ encoding / xmlパッケージ。 (* xml.Decoder).Tokenを呼び出すたびにトークンが返されます。 APIの関連部分は次のとおりです。

package xml

type Name struct {
  Local string // e.g., "Title" or "id"
}
type Attr struct { // e.g., name =" value"
  Name Name
  Value string
}

// A Token includes StartElement, EndElement, CharData, 
// and Comment, plus a few esoteric types (not shown). 
type Token interface{}
type StartElement struct { // e.g., < name > 
  Name Name
  Attr [] Attr
}
type EndElement struct { Name Name } // e.g., </ name >
type CharData [] byte // e.g., < p > CharData </ p >
type Comment [] byte // e.g., <!-- Comment -->
type Decoder struct{ /* ... */ }
func NewDecoder( io.Reader) *Decoder
func (* Decoder) Token() (Token, error) // returns next Token in sequence

メソッドを持たない Tokenインタフェースも、識別された共用体の例です。 `io.Reader 'のような伝統的なインタフェースの目的は、新しい実装を作成できるように、それを満たす具体的な型の詳細を隠すことです。 各コンクリート型は一様に処理される。 対照的に、識別された共用体を満たす具体的な型の集合は、設計によって固定され、隠されているのではなく公開されます。 差別化された共用体型にはメソッドがほとんどありません。 それらの上で動作する関数は、それぞれ異なる論理を持つ型スイッチを使用して一連のケースとして表現されます。

以下のxmlselectプログラムは、XMLドキュメントツリーの特定の要素の下にあるテキストを抽出して出力します。 上記のAPIを使用すると、ツリーをマテリアライズすることなく、入力上で1回の処理でジョブを実行できます。...

https://github.com/adonovan/gopl.io/ch7/xmlselect

// Xmlselect prints the text of selected elements of an XML document.

package main

import (
 "encoding/ xml"
 "fmt"
 "io"
 "os"
 "strings"
)

func main() { 
  dec := xml.NewDecoder( os.Stdin)
  var stack [] string // stack of element names
  for {
     tok, err := dec.Token()
     if err = = io.EOF {
       break
     } else if err != nil {
       fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err)
       os.Exit(1)
     }
     switch tok := tok.(type) {
     case xml.StartElement:
       stack = append(stack, tok.Name.Local) // push
     case xml.EndElement:
        stack = stack[:len( stack)-1] // pop
     case xml.CharData:
      if containsAll(stack, os.Args[1:]) {
        fmt.Printf("% s: %s\n", strings.Join(stack, " "), tok)
      }
     }
   }
}

// containsAll reports whether x contains the elements of y, in order.
func containsAll(x, y [] string) bool {
  for len(y) < = len(x) {
    if len(y) == 0 {
      return true
    }
    if x[0] == y[0] {
     y = y[1:]
    }
    x = x[1:]
  }
  return false
}  

mainのループが StartElementに遭遇するたびに、それは要素の名前をスタックにプッシュし、各 EndElementに対してスタックから名前をポップします。 APIは、不正な形式の文書であっても、「StartElement」とEndElementトークンの順序が適切に一致することを保証します。 Commentsは無視されます。 xmlselect CharDataを検出すると、スタックにコマンドライン引数で指定されたすべての要素が順番に含まれている場合にのみ、テキストが出力されます。

以下のコマンドは、 div要素の2つのレベルの下に現れる h2要素のテキストを表示します。 その入力はXML仕様であり、それ自体がXML文書です。

$ go build gopl.io/ch1/fetch
$ ./ fetch http://www.w3.org/TR/2006/REC-xml11-20060816 | ./ xmlselect div div h2
html body div div h2: 1 Introduction
html body div div h2: 2 Documents
html body div div h2: 3 Logical Structures
html body div div h2: 4 Physical Structures
html body div div h2: 5 Conformance 
html body div div h2: 6 Notation 
html body div div h2: A References 
html body div div h2: B Definitions for Character Normalization
....

7.15 A Few Words of Advice

新しいパッケージを設計する場合、初心者のGoプログラマはしばしば一連のインタフェースを作成し、後でそれらを満たす具体的な型を定義することから始めます。このアプローチは多くのインタフェースをもたらし、それぞれが単一の実装しか持たない。それをしないでください。そのようなインタフェースは不要な抽象化です。ランタイムコストもあります。エクスポートメカニズム(6.6節)を使用して、構造体の1つまたは複数のメソッドのメソッドがパッケージの外に見えるように制限することができます。インターフェイスは、一意の方法で扱わなければならない2つ以上の具体的な型がある場合にのみ必要です。

1つの具体的な型でインターフェイスが満たされているが、その型がその依存関係のためにインターフェイスと同じパッケージに存在できない場合、このルールの例外を作成します。その場合、インターフェースは2つのパッケージを切り離す良い方法です。

インタフェースは、2つ以上の型で満たされている場合にのみGoで使用されるため、必ずしも特定の実装の詳細から抽象的に抽象化されています。結果として、より少ない、より単純なメソッドで、より小さいインタフェースがあり、 io.Writer fmt.Stringerのように1つだけであることがよくあります。小さなインターフェースは、新しいタイプが登場したときに満たすのが簡単です。インターフェイス設計の経験則は、必要なものだけを尋ねることです。

これで、メソッドとインタフェースのツアーを終了します。 Goは、オブジェクト指向のプログラミングスタイルを大きくサポートしていますが、これを排他的に使用する必要はありません。すべてがオブジェクトである必要はありません。スタンドアロン関数は、カプセル化されていないデータ型と同じように、その場所を持っています。この本の最初の5つの章の例では、 fmt.Printfのような通常の関数呼び出しとは対照的に、 input.Scanのようなメソッドは20ダース以上しか呼び出していないことに注意してください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment