Skip to content

Instantly share code, notes, and snippets.

@tokubass
Last active October 24, 2016 12:17
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/ed369a271c63d9cad10f2452989dfcc3 to your computer and use it in GitHub Desktop.
Save tokubass/ed369a271c63d9cad10f2452989dfcc3 to your computer and use it in GitHub Desktop.

#7 Interfeace interface型は、他の型のふるまいについて、汎化や抽象を表す。 汎化によって、特定のひとつの実装に日も付かないので、interfaceは私たちにmore flexibleで、adaptableな関数を書かせてくれる。

多くのオブジェクト志向言語はいくつかのinterfaceの概念をもっているが、Goのinterfaceが独特なのは、それらが暗黙的に満たされている点。つまり、与えられた具象型を満たす、すべてのinterfaceを宣言する必要がない。単純に必要なメソッドを所持するだけです。このデザインは 既存の型(特にあなたがコントロールできないpackage内に定義されている型に有用)の変更なしに、既存の具象型によって満たされる新しいインタフェースを作らせてくれる。

この章では、interface型の基本的な仕組みと値をみることから始めます。 途中で、standard libraryからいくつかの重要なinterfaceを学びます。多くのGo プログラムはそれら自身がそうであるように、多くの標準的なインタフェースを多く使おうとします。

最後に、type assertions(7.10) と type switches(7.13)と、それらが汎化の異なる種類を有効にする方法について見ていきます。

7.1 Interfaces as Contracts

これまで見てきた全ての型は具象型でした。具象型は、値の正確な表現を明記し、その表現固有のオペレーションを露出します。たとえば、数値の算術計算、indexing,append,sliceのrangeがあります。具象型はメソッドを使って追加の振る舞いを与えることもできる。具象型の値をもっているとき、それが何であるか、何ができるのか、を完全に知ることができる。

Goにはinterface typeと呼ばれる他の種類の型がある。interfaceは抽象型。 表現や値の内部構造、それらがサポートする基本操作の集合を露出しない。メソッドだけを公開する。あなたがinterface型の値をもっているとき、あなたはそれが何であるかはまったく知らない、ただ何ができるかを知っているだけ。もっと正確にいえば、メソッドによって与えられた振る舞いである。

この本のいたるところで、私たちはstring formattingのための似た二つの関数を使っています。 fmt.Printfはstandard output(a file)に結果を書き込み、fmt.Sprintfはstringとして結果をreturnします。もし結果のformattingが難しいところなら残念なことです。表面上、結果の使われ方が異なるので重複しなければならない。interfaceのおかげで、そうではない。それらの関数は両方とも、実際には、fmt.Fprintfのラッパー。それは計算結果がどうなるかについて、とらわれません。

FprintfのF prefixはファイルを表し、formatされた出力が第一引数として与えられたファイルに書かれるべきということを示している。Printfの場合、引数は*os.FIle型のos.Stdout。Sprintfの場合、引数はファイルではない。しかしながら、それは表面的にはFprintfと似ている。&bufはbytesを書きこめるmemory bufferへのポインタ。

Fprintfの最初のパラメータもファイルではない。io.Writerは次の宣言を伴うinterface型です。

このio.WriterインターフェイスはFprintfと呼び出し元との契約を定義する。一方では、契約は、*os.Fileや *byte.Bufferのような呼び出し側が与えた具象型の値がWriteと名付けられた適切なsignatureと振る舞いをもつメソッドを持つことを必要とします。他方では、契約は、Fprintfがio.Writerインタフェースを満たす任意の値から与えられた仕事をすることを保証します。Fprintfがファイルかメモリに書き込むことを仮定しないでください。ただWriteが呼び出し可能なだけです。

理由は、fmt.Fprintfは値の表現について仮定せず、ただio.Writer契約によって保証される振る舞いにのみ依存するからです。私たちはfmt.Fprintfへの第一引数として、io.Writerを満たす任意の具象型の値を安全に受け渡すことができる。この自由は置換可能性 (substitutability ) と呼ばれる同じインタフェースを満たす他の型の代わりに片方の型を使うこと。これはOOPの顕著な特徴。

新しい型をつかってこれを試してみましょう。以下の*ByteCounter型のWriteメソッドは単に、それらが廃棄される前に、書き込まれたbytesをカウントします。

*ByteCounterはio.Writer契約を満たすので、私たちはそれをFprintfに渡すことができる。Fprintfはこの変更には気付かずにstring fomattingがおこなわれます。ByteCounterは正しく結果の長さを保存します。

io.Writer以外に、fmtパッケージに非常に重要な別のインタフェースがある。Fprintf,Fprintlnは値がどのようにprintされるのかをコントロールする方法を型に与える。セクション2.5では、温度を100℃と印字できるようにCelsius型にStringメソッドを定義した。セクション6.5では、私たちは{1 2 3}のような伝統的なセット表記を使用してレンダリングされるようにstringメソッドとともに*IntSetを実装した。文字列メソッドの宣言は、すべてのもっとも広く使われるインタフェースのいずれかを満たすことになります。fmt.Stringer:

私たちは、どの値がこのinterfaceを満たすのかをfmt packageがどうやって発見するのかをsection 7.10で説明します。

7.2 Interface Types

io.Writer型はもっとも広く使われるインタフェースのひとつです。なぜなら、それはbyteを書き込むことができる、すべての型の抽象を与えるから。file,memory,buffer,network,connection,HTTP client, archiver,hasher,....etc.. io パッケージは多くの有用なパッケージを定義している。 Readerは byteを読むことができる任意の型を表す。Closer はcloseできる任意の値、たとえばfile,network,connection.(Goの多くのインタフェースのシングルメソッドの命名規則にそろそろ気づいたのではないでしょうか)

既存の型の組み合わせで新しいインタフェースを宣言する。

type ReadWriter interface {
	Reader
	Writer
}
type ReadWriteCloser interface {
	Reader
	Writer
	Closer
}

上記のシンタックスは、構造体埋め込みと似ている。 メソッドをすべて書き出す(Reader,Writer,Closer)ためのショートカットとして別のインタフェース(ReadWriteCloser)を名付けた

埋め込みなしでも宣言できるし、mixすることもできる。すべての同じものを表す methodの順序は重要ではありません。

type ReadWriter interface {
	Read(p []byte) (n int, err error)
	Writer
}

7.3 Interface Satisfaction

型はインタフェースが要求するメソッドをすべて持っていれば、インタフェースを満たす。 例えば、 *os.Fileはio.Reader,Writer,Closer, ReadWriterを満たす。 *bytes.BufferはReader,Writer,ReadWriterを満たす。しかしCLoserは満たさない。なぜなら、Closeメソッドはもってないから。 簡潔な表現として、Goプログラマーは具象型は、そのインタフェースを満たすことを意味し、特別なインタフェース型であると良く言います。例えば, *bytes.Bufferはio.Writer, *os.Fileはio.ReadWriterです。 (?? 下のサンプルコードで意味がわかった。var w io.Writerで宣言してる変数に、bytes.Bufferのインスタンスを代入している。)

このinterfaceのためのアサインルール(2.4.2)はとても単純です。式はinterfaceに代入することができます。もし型がインタフェースを満たすのなら。

var w io.Writer 
w = os.Stdout // *os.FileはWriterメソッドをもつ
w = new(bytes.Buffer) // *bytes.BufferはWriteメソッドをもつ
w = time.Second // Writeメソッドをもってないのでcompile error 

このルールは右辺がインタフェースのときでさえ適用されます。

さらに先に進む前に、私たちは型がメソッドをもつことが何を意味するのかの詳細を説明すべきです。 セクション6.2を思い出してください。それぞれ具象型Tと名付けられている。そのメソッドのいくつかはT型のレシーバーをもっている。他が*Tポインターを要求しているのに対して。 T型の引数が変数であるかぎり、その上で *T メソッドを呼び出すことは合法です。コンパイラは暗黙的にアドレスを取得します。しかしこれは単なるシンタックスシュガーです。T型の値は すべての*Tポインタのメソッドを保持しません。結果として、少数のinterfaceを満たすかもしれません。 この例はこれを明確にするでしょう。section 6.5のIntSetのStringメソッドは、ポインターレシーバーを要求します。したがって、私たちはアドレス不可能なIntSet値を呼び出すことはできません。

type IntSet struct { /* ... */ }
func (* IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiv
var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method

しかしながら、*IntSetはStringメソッドを持っているだけなので、fmt.Stringerインタフェースだけを満たす。

var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error : IntSet lacks String method

godoc - analysis=type tools (10.7.4)

インタフェースは具象型と値を隠蔽し、メソッドはインタフェースによってのみ公開される。 たとえ、その具象型が他を所持していても。

os.Stdout.Write([]byte(" hello")) // OK: *os.File has Write method
os.Stdout.Close() // OK: *os.File has Close method

var w io.Writer
w = os.Stdout
w.Write([] byte(" hello")) // OK: io.Writer has Write method
w.Close() // compile error: io.Writer lacks Close method

empty interface

var any interface{} なんでも入る。section5.7のfmt.Println , errofで登場した。

任意の*bytes.Buffer型の値から新たに変数をallocateする必要はありません。nillでさえ。

var w io.Writer = new(bytes.Buffer)

(*bytes.buffer)(nill)は明示的な変換。 私たちは決してwを参照するつもりがないので、それをblank identifierと置き換えることができる。

var _ io.Writer = (*bytes.Buffer)(nil)

io.Writerのような空でないinterfaceはよくポインター型によって満たされる。 とりわけ、Writeメソッドのように、一つ以上のインタフェースメソッドがレシーバーに対して副作用のある種類であることを暗示する場合。 構造体へのポインターは、とりわけ、ありふれたメソッドを備えた型。 しかしpointer型は決して、インタフェースを満たすためだけの型ではない。 mutator methodsをともなうインタフェースもまた、Goの異なる参照型によって満たすことができる。(例として、6.1では、slice型がつかわれている。6.2.1ではmap型, 7.7では関数型) 基本型でさえ、interfaceを満たすことができる.7.4でみていく。(time.Durationはfmt.Stringerを満たす)

具象型は多くの無関係なインタフェースを満たす。音楽,映画、本のようなデジタル化された文化財を、整理、販売するプログラムを考えてください。 おそらく以下のような具象型を定義するでしょう。

  • Album
  • Book
  • Movie
  • Magazine
  • Podcast
  • TVEpisode
  • Track

私たちは各関心事の抽象化をインタフェースとして、表現することができる。タイトルや作成日のようないくつかのプロパティはすべてのartifactで共通です。

type Artifact interface {
	Title () string
	Creators() []string
	Created() time.Time
}

他のプロパティはartifactの型に制限される。 印字するためのプロパティはbook,magazineだけに関連する。movie,TV episodeはscreen関連をもつ。

type Text interface {
	Pages() int
	Words() int
	PageSized() int
}

type Audo interface {
	Stream() (io.ReadCloser, error)
	RunningTime() time.Duration
	Format() string // MP3,WAV
}
type Video interface {
	Stream() (io.REadCloser,error)
	RunningTime() time.Duration
	Format() string // MP4,WMV
	Resolution() (x,y int)
}

関連する具象型のグループのために有用です。

例えばAudio,Videoのアイテムを同じ方法で探す必要があるとすると、既存の定義型を変更しない、共通aspectを表現するStreamerインタフェースを定義することができる。

type Streamer interface {
	Stream() (io.ReadCloser,error)
	RunningTime() time.Duration
	Format() string
}

具象型のグルーピングは、interfaceとして表現することができる共通する振る舞いに基づいている。 インタフェースの集合が明示的なクラスによって満たされるclassベースの言語とは違って、 Goは具象型の定義の変更なしに、必要なときに、関心のグルーピング化や新しい抽象化を定義できる。 これは特に、異なる著書によって書かれたパッケージからもたらされた具象型のときに役にたつ。 もちろん、具象型の中の、基本的な性質を共有している必要があります。

#7.4 Parsing Flags with flag.Value

https://github.com/adonovan/gopl.io/blob/master/ch7/sleep/sleep.go

sleepする前に、time.DurationのStringメソッドを呼び出す。これは、nanosecondsの数字としてprintされず、user-friendlyな表現になる。 デフォルトではsleepの期間は1sですが、-period コマンドラインフラグを使ってコントロールすることができる。 flag.Duration関すhはtime.Duration型のflag変数を作成する。 flag packageはbuiltin 新たに型を追加することも容易。flag.Valueインタフェースを満たすだけでいい。

package flag

type Value interface {
	String() string
	Set(string) error
}

Stringメソッドはコマンドラインのヘルプメッセージのために、flag値をフォーマットする。 このようにすべてのflag.Valueはfmt.Stringerでもあります。 Setメソッドは引数をパースし、flag値を更新します。実際には、SetメソッドはStringメソッドの逆です。これらは同じ記法あることがgood practiceです。

celsiusFlag型を定義してみましょう。Celsius型を組み込んでいるものとします。CelsiusはStringメソッドをもっているので、必要なのはSetだけです。

https://github.com/adonovan/gopl.io/blob/master/ch7/tempconv/tempconv.go

fmt.Sscanfで入力値をパース。このとき通常はエラー結果をチェクする必要があるが、今回の場合、switchでマッチする箇所がないので、必要ない。 celsiusFlagは以下のすべてをラップします。呼び出し側のため、celsiusFlagのf変数に組み込まれているCelsiusフィールドへのポインターを返却します。 Celsiusフィールドはflag処理の期間、Setメソッドによってアップデートされる変数です。global変数のflag.CommandLineのVarの呼び出しは、アプリケーションのコマンドラインフラグ集合へのflag追加です。 異常に複雑なコマンドラインインタフェースをもつプログラムでは、この型の変数をいくつかもつことができます。Var呼び出しで*celsiusFlag引数をflag.Valueパラメータに割り当てます。*celsiusFlagが必要なメソッドをもっていないとcompile errorになります。

使用するコード https://github.com/adonovan/gopl.io/blob/master/ch7/tempflag/tempflag.go

7.5 Interface Values

概念上は、interface型の値、もしくはinterface valueは、具象型とその値の2つのコンポーネントを持ちます。 それらはインタフェースのdynamic typedynamic valueと呼ばれます。

Goのような性的な型の言語では、型はコンパイル時コンセプトです。だから型は値ではない。私たちのコンセプトモデルは、type descriptorsと呼ばれる値の集合が、各型についての名前やメソッドのような情報を提供する。インタフェース値では型コンポーネントは適切なtype descriptorによって表現される。

以下では、wは3つのことなる値を取得している。

var w io.Writer // 値は w = nill と同じ
w = os.Stdout
w = new(bytes.Buffer)
w = nill

各記述の後のwの値と動的な振る舞いをもっと見てみよう。 最初の宣言部分であるvar w io.Writer Goでは変数はいつも明確に定義された値で初期化される。interface�も例外ではない。 interfaceのためのzero valueは型と値のコンポーネント、両方ともnilがセットされる。

interface値はそのdynamic typeに基づいてnillかnon-nilとして記載されます。 ですので、これはnil interface value です。 w == nil w != nilでinterface valueをテストできます。nil interfaceのメッソッドを呼び出すとpanicを引き起こします。

2番目 w = os.Stdout // type is *os.File この代入は具象型からinterface型への暗黙的な変換を伴う。これは明示的な変換 io.Writer(os.Stdout)と同等です。 この種の変換は明示的か暗黙的かにかかわらず、そのオペランドの型と値を保存します。 interface値のdynamic typeは*os.Fileのためにtype descriptorにセットされます。かつdynamic valueはos.Stdoutのコピーを保持します。これはos.File変数へのポインタです。

*os.Fileポインターを含むinterface valude上でWriteメソッドを呼び出すと、(*os.FIle).Writeメソッドが呼び出されます。

一般的に、コンパイル時にinterface値のdynamic typeが何であるか知ることはできません。 そのためinterfaceを通じた呼び出しは、dynamic dispatchを使わなければなりません。 direct callの代わりに、コンパイラーはtype descriptorからWriteと名付けられてたメソッドのアドレスを取得するためにコード生成しなければなりません。その後、そのアドレスに間接的な呼び出しがされます。呼び出しのためのレシーバー引数はinterfaceの動的値(os.Stdout)のコピーです。 直接os.Stdout.Write([]byte("hello"))を呼び出したかのうような効果があります。

3番目。w = new(bytes.Buffer) *bytes.Buffer型の値をinterface valueに代入している。 dynamic typeは現在*bytes.Bufferであり、かつdynamic valueは新たにallocateされたbufferへのポインターです。 Writeメソッドを呼び出しは前回と同じメカニズムが使われます。 今回は、type descriptorは*bytes.Buffer,つまり、(*bytes.Buffer).Writeメソッドがレシーバーパラメータの値としてbufferのアドレスとともに、呼ばれる。この呼び出しはbufferにhelloを追加する。

最後にinterface valudeにnilを代入する場合。 最初に宣言したときと同じく、両方のコンポーネントにnilがセットされる。

interface valueは多くのdynamic valueを任意に維持する。たとえばtime.Time型はtimeのインスタンスを表現する。これはいくつかのunexportなフィールドをもつ構造体です。 もしこれからinterface valueを生成すると、 var x interface{} = time.Now() この結果はfigure 7.4のようになるでしょう。コンセプトとして、dynamic valueはいつもinterface valueを内蔵する。どんなに大きな型であっても。(これはコンセプトに限った話で、現実的な実装はまったく違う。)

interface valuesは==,!=で比較することができる。2つのinterface valuesがもし両方ともnilか、もしくはdynamic typesが同じかつ、dynamic valuesがその型の==の通常の振る舞いによって等しいなら、等しい。 interface valuesは比較可能なので、それらはmapのkeyやswitch statementのoperandとして使える。

しかしながら、もし二つのinterface valuesが比較可能かつ同じdynamic typeをもっていても、その(dynamic)型が比較可能でなければpanicになる。

var x interface{} = []int{1, 2, 3}
fmt.Println(x == x ) //panic: comparing uncomparable type []int

この点において、interface typesは独特です。他の型は安全に比較可能(basic type or pointer)か、まったく比較できない(slice,map,function)かのどちらか。 しかし、interface値またはinterface値を含む集合型を比較するときは、私たちはpanicが発生する可能性に気づかなければならない。mapのkeyやswitchのoperandとしてinterfaceを使うと同様のリスクがある。 interface valuesを比較するのは、それらが比較可能な型のdynamic valuesを含むことを確信しているときのみです。

エラーをハンドリングするとき、またはdebugging中のとき、interface valueのdynamic typeを報告することはしばしば役に立つ。そのために、私たちは、fmtの%T動詞?を使う。 内部的に、fmtはinterfaceのdynamic typeの名前を取得するためにreflectionを使います。reflectionについてはChapter 12で見ていきます。

7.5.1 警告: An Interface Containing a Nil Pinter Is Non-Nil

すべてのnil interfaceは値をまったく含まない。interfaceが保持していたポインタの値が図らずもNilになるのとは異なります。この微妙な違いはトラップを生みます。すべてのGo Programmerがつまずかされた。

const debug = true
func main() {
   var buf *bytes.Buffer
   if debug {
   	buf = new(bytes.Buffer) //enable collection of output
   }
   f(buf) // NOTE: subtly incorrect!
   if debug {
   	  // ...use buf ...
   }
   
   func f (out io.Writer) {
      // ...do something ...
      if out != nil {
        out.Write([]byte("done!\n"))
      }
   }
   	

debugがfalseのとき、outpuの収集?は無効化になることを期待している。しかしout.Writeのどこかでpanicが発生する。 mainがfをcallしたとき、nil pointer型の*bytes.Bufferがoutパラメータに代入される。したがって、outのdynamic valueはnil。しかしながらdynamic typeは*bytes.Buffer. つまりoutは、nil pointer valueを含むnon-nil interfaceである。(図7.5), したがって防衛的なチェックout != nil はいまだtrueです。 すでに述べた通り、dynamic dispatch mechanismは 呼び出されるべき(*byres.Buffer).Writeを決定しますが、今回はreceiver valueがnil。*os.Fileのような、いくつかの型については、nilはvalidなreceiverです。($6.2.1)しかし*bytes.Bufferは含まれません。メソッドは呼ばれますが、bufferにアクセスしようとして、panicになる。

この問題はnil *bytes.Buffer pointerがinterfaceを満たすために必要なメソッドをもっているにもかかわらず、interfaceのbehavioral必須条件を満たさなかったことです。 特に、callは(*bytes.Buffer).Writeがnot nilであるという暗黙の事前条件に違反します。したがって、interfaceにアサインされたnil pointerは誤りでした。解決方法はmain内のbufの型をio.Writerに変更することです。それによって、最初の段階で、interfaceに機能不全な値をアサインすることを避ける。

今や私たちは、interface valueのメカニズムについてカバーしているので、Goのstandard libaryからさらに重要なinterfaceを少し見てみよう。次の3セクションでは、sorting,web serving, error handlingでどのようにinterfaceを使うのかみていく。

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