グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



<Goのパッケージ放浪記> ioパッケージに定義されている「Closerインターフェイス」について

今日は前回のWriterインターフェイスに続き、ioパッケージのCloserインターフェイスまわりを覗いていきます。

今回覗いていくソースのバージョンは「go1.14.4」です。

Closerインターフェイス

早速Closerインターフェイスの内容をみていきます。

// Closer is the interface that wraps the basic Close method.
//
// The behavior of Close after the first call is undefined.
// Specific implementations may document their own behavior.
type Closer interface {
	Close() error
}

Closerの初回以降の呼び出しはどうなるか定義されてないようです。

Closerインターフェイスの実装を確認する

Closerインターフェイスの実装として今回は、osパッケージのFileを調査して実装を探っていきます。

まず見ていくファイルは、 os/types.go です。

// File represents an open file descriptor.
type File struct {
	*file // os specific
}

こちらにFile構造体が定義されています。
 *file にはOS固有の構造体を指定して埋め込み(Embedding)しているようです。

今回はLinux環境での file の定義を見たいので、 os/file_unix.go を見ていきます。
このファイルは冒頭にビルド制約(ビルドタグ)が書かれていて、特定の条件時のみビルド対象にするように設定されています。

// +build aix darwin dragonfly freebsd js,wasm linux netbsd openbsd solaris
 file 構造体の定義は以下の通りです。

// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
	pfd         poll.FD
	name        string
	dirinfo     *dirInfo // nil unless directory being read
	nonblock    bool     // whether we set nonblocking mode
	stdoutOrErr bool     // whether this is stdout or stderr
	appendMode  bool     // whether file is opened for appending
}

pollやepollのファイルディスクリプタと思われる pfd を持っています。
 file 構造体は file 構造体に埋め込まれているので、 file 構造体を通してCloseメソッドを実装しているのがわかります。

// Close closes the File, rendering it unusable for I/O.
// On files that support SetDeadline, any pending I/O operations will
// be canceled and return immediately with an error.
// Close will return an error if it has already been called.
func (f *File) Close() error {
	if f == nil {
		return ErrInvalid
	}
	return f.file.close()
}

func (file *file) close() error {
	// ファイルが無い場合はエラーを返す
	if file == nil {
		return syscall.EINVAL
	}
	// ディレクトリ情報を持っている場合はそちらもCloseする
	if file.dirinfo != nil {
		file.dirinfo.close()
	}
	var err error
	// file構造体のpfdのCloseメソッドを呼び出す
	if e := file.pfd.Close(); e != nil {
		// エラーがpoll.ErrFileClosing変数と同じ場合はeにエラーを設定する
		if e == poll.ErrFileClosing {
			e = ErrClosed
		}
		// PathError構造体に、行った操作、パス、エラーを設定する
		err = &PathError{"close", file.name, e}
	}

	// no need for a finalizer anymore
	// fileに関連付けられているすべてのfinalizerをクリアする
	runtime.SetFinalizer(file, nil)
	return err
}
 file.pfd のCloseメソッドを呼び出すのがメインの処理です。
こちらも、Reader、Writer同様に同じインターフェイスを呼び出しています。
この先の処理も気になるのですが、今回の目的から反れるのでここまでにしておきます。

失敗した場合の処理は、 pfd が定義されているpollパッケージ内の変数とエラーを比較しています。
 poll.ErrFileClosing と同じ場合は、 e  ErrClosed にしてPathError構造体に入れています。

PathError構造体は以下のようになっています。

// PathError records an error and the operation and file path that caused it.
type PathError struct {
	Op   string
	Path string
	Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

func (e *PathError) Unwrap() error { return e.Err }

行った操作や、発生したパス、エラーを含む構造体です。

最後に、Closeメソッドの成功、失敗にかかわらず SetFinalizer 関数で、すべてのファイナライザをクリアしているのがわかります。

For example, an os.File object could use a finalizer to close the associated operating system file descriptor when a program discards an os.File without calling Close, but it would be a mistake to depend on a finalizer to flush an in-memory I/O buffer such as a bufio.Writer, because the buffer would not be flushed at program exit.

こちらの説明によると、Closeを呼ばなかった場合としてファイナライザを設定しているようです。
明示的にCloseした場合は、保険用のファイナライザが必要なくなるのでクリアしているのかもしれません。
このあたりの挙動は、fileやpollパッケージを覗くときに理解していきたいと思います。

まとめ

Closeメソッド自体はシンプルでしたが、それを取り巻くFile構造体まわりはOSの差分を吸収するために、ビルド制約や構造体の埋め込みを利用して対応しているのが理解できました。
ファイルディスクリプタを扱う部分はシステムコールが多く、理解しにくそうですが追々覗いていきたいと思います。
細かいところでは、エラーの判別やラップの仕方が勉強になりました。

この記事を書いた人

tkr2f
tkr2f事業開発部 web application engineer
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします