フューチャー技術ブログ

Go1.16からのio/ioutilパッケージ

こんにちは、TIGの辻です。Go 1.16連載の3記事目です。

Go1.16でアップデートがあった io/ioutil パッケージが "deprecated" になる話題のまとめです。

サマリ

  • Go1.16から io/ioutil パッケージの機能が osio パッケージに移行した
  • これから新しく実装するコードは ioos パッケージの新しい関数を使うことが推奨される
  • io/ioutil パッケージが "deprecated" になるが "deprecated" といっても将来壊れる、ということではない
    • 既存のコードは動作し続ける
  • go fix コマンドは未対応

内容

Go1.16から io/ioutil パッケージに含まれる関数が "deprecated" になります。関連するプロポーザルは #40025#42026 です。Package names で良くないパッケージ名として紹介されているように、一般的には util などというパッケージ名は、純粋にユーティリティを提供するパッケージではない限り避けるべき名前です。io/ioutil の命名に関しては #19660 にあるように以前から理解しにくいと言われていました。

io/ioutil パッケージは主にパッケージのインポートサイクルを回避するために存在します。Codebase Refactoring にあるようにGoの io パッケージは os パッケージに依存することはできません。io/ioutil パッケージが直接/間接的に os パッケージと io パッケージを参照することで、インポートサイクルを回避したということです。

さて io/ioutil では以下の8つの型/関数がExportされています。

  • Discard
  • NopCloser
  • ReadAll
  • ReadDir
  • ReadFile
  • TempDir
  • TempFile
  • WriteFile

これらのすべての型/関数が "deprecated" になり、io パッケージと os パッケージに機能が移動します。

  • io パッケージに移動する型/関数
    • Discard
    • NopCloser
    • ReadAll
  • os パッケージに移動する関数
    • ReadDir
    • ReadFile
    • TempDir -> MkdirTemp (リネーム)
    • TempFile -> CreateTemp (リネーム)
    • WriteFile

io パッケージに移動する型/関数

io パッケージに移動する型/関数は以下の3つです。

  • Discard
  • NopCloser
  • ReadAll

#40025 によると DiscardNopCloser はたまたま io/ioutil パッケージに含まれてしまった、とのことです。ReadAll も本来であれば ReaderWriter のヘルパー関数が io パッケージで提供されていることにならって io パッケージに含まれる機能でしたが、bytes.Buffer にアクセスする実装となっていており、bytes パッケージが os パッケージに依存する関係で io/ioutil パッケージに追いやられました。

しかしながら ReadAllbytes.Buffer を使う必要がないため、Go1.16の ReadAll の実装は bytes パッケージを使わない実装になっています。

io/ioutil/ioutil.go
// readAll reads from r until an error or EOF and returns the data it read
// from the internal buffer allocated with a specified capacity.
func readAll(r io.Reader, capacity int64) (b []byte, err error) {
var buf bytes.Buffer
// If the buffer overflows, we will get bytes.ErrTooLarge.
// Return that as an error. Any other panic remains.
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
}
}()
if int64(int(capacity)) == capacity {
buf.Grow(int(capacity))
}
_, err = buf.ReadFrom(r)
return buf.Bytes(), err
}

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r io.Reader) ([]byte, error) {
return readAll(r, bytes.MinRead)
}
  • Go1.16の io.ReadAll の実装(io/io.go)
io/io.go
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {
b := make([]byte, 0, 512)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
}
}

バイトスライスの容量を拡張するときに一旦 byte 型のゼロ値0をバイトスライスにappendして容量を拡張し、長さは元のスライスの長さに戻すことでスライスの容量だけ予め拡張するオシャレな実装になっています。

io/ioutil パッケージからは io.ReadAll に委譲するように実装されています。

io/ioutil/ioutil.go
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
//
// As of Go 1.16, this function simply calls io.ReadAll.
func ReadAll(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}

os パッケージに移動する関数

os パッケージに移行する関数は以下の5つです。io/ioutil パッケージに含まれていた下記の関数はOSファイルシステムのヘルパー関数です。

  • ReadDir
  • ReadFile
  • TempDir -> MkdirTemp (リネーム)
  • TempFile -> CreateTemp (リネーム)
  • WriteFile

リネーム

ioutil の2つの関数がリネームになっています。TempDir 関数は既に os パッケージに存在します。os.TempDir 関数はOSのデフォルトの一時ディレクトリを返却するAPIです。ioutil パッケージの TempFile は今回移行した os パッケージでは MkdirTemp という関数になっています。また MkdirTemp との命名の整合をとるために ioutil パッケージに存在していた TempFileCreateTemp という命名になっています。

シグネチャ

ReadDir は返却するシグネチャが更新されています。移行後の ReadDirfs.FileInfo ではなく os.DirEntry を返却するようになっています。

os/dir.go
// ReadDir reads the named directory,
// returning all its directory entries sorted by filename.
// If an error occurs reading the directory,
// ReadDir returns the entries it was able to read before the error,
// along with the error.
func ReadDir(name string) ([]DirEntry, error) {
f, err := Open(name)
if err != nil {
return nil, err
}
defer f.Close()

dirs, err := f.ReadDir(-1)
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
return dirs, err
}
io/ioutil/ioutil.go
// ReadDir reads the directory named by dirname and returns
// a list of fs.FileInfo for the directory's contents,
// sorted by filename. If an error occurs reading the directory,
// ReadDir returns no directory entries along with the error.
//
// As of Go 1.16, os.ReadDir is a more efficient and correct choice:
// it returns a list of fs.DirEntry instead of fs.FileInfo,
// and it returns partial results in the case of an error
// midway through reading a directory.
func ReadDir(dirname string) ([]fs.FileInfo, error) {
f, err := os.Open(dirname)
if err != nil {
return nil, err
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil, err
}
sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
return list, nil
}

"deprecated" とは

"deprecated" とあるため、将来的に ioutil パッケージを使っているプログラムが動作しなくなるのでは? と思う方もいるかもしれません。"deprecated" が何を意味するのか Russ Cox 氏がツイートしています。

すわなち「より良い、好ましい方法がある」という意味で「将来的に壊れる」という意味ではない、ということです。Go 1 and the Future of Go Programs にもあるように、少なくともGo1の間はソースレベルの後方互換性が保たれます。

ioutil パッケージの関数はとても便利ですので、多くの実装で使用されていますが、ioutil パッケージを使っている既存の実装は引き続きそのまま動作します。新規に実装する場合は os パッケージや io パッケージに含まれる関数を利用するのが推奨されています。

"deprecated" になったパッケージの移行

go fix を使うと、古いAPIを使用しているGoのプログラムを検出し、新しいAPIに書き換えることができます。馴染みがあるAPIだと context パッケージがあげられます。もともとは golang.org/x リポジトリ配下の準標準ライブラリとして実装されていた context パッケージですが、Go1.7以降は標準ライブラリに移行しています。以下のファイルがあった場合に go tool fix -diff main.go とすると新しいAPIに書き換えることができます。

  • main.go
package main

import "golang.org/x/net/context"

func main() {
ctx := context.Background()
doSomething(ctx)
}

func doSomething(ctx context.Context) {
// doing something
}

Diffを確認できます。-diff オプションを除けば、ファイルを直接更新できます。

$ go tool fix -diff main.go
main.go: fixed context
diff main.go fixed/main.go
--- /tmp/go-fix572668930 2021-02-06 08:48:06.046862800 +0900
+++ /tmp/go-fix017905529 2021-02-06 08:48:06.048069100 +0900
@@ -1,6 +1,6 @@
package main

-import "golang.org/x/net/context"
+import "context"

func main() {
ctx := context.Background()

ioutil パッケージの go fix に関して #42026 で言及されています。残念ながら #32816 のプロポーザルには含まれない だろう、とコメントしています。将来的に go fix コマンド一発で既存の ioutil パッケージを使っているAPIから io パッケージや os パッケージのAPIへ移行ができると嬉しいですね。

参考