こんにちは、TIGの辻です。Go 1.16連載の3記事目です。
Go1.16でアップデートがあった io/ioutil パッケージが "deprecated" になる話題のまとめです。
サマリ
- Go1.16から
io/ioutilパッケージの機能がosとioパッケージに移行した - これから新しく実装するコードは
ioやosパッケージの新しい関数を使うことが推奨される 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されています。
DiscardNopCloserReadAllReadDirReadFileTempDirTempFileWriteFile
これらのすべての型/関数が "deprecated" になり、io パッケージと os パッケージに機能が移動します。
ioパッケージに移動する型/関数DiscardNopCloserReadAll
osパッケージに移動する関数ReadDirReadFileTempDir->MkdirTemp(リネーム)TempFile->CreateTemp(リネーム)WriteFile
io パッケージに移動する型/関数
io パッケージに移動する型/関数は以下の3つです。
DiscardNopCloserReadAll
#40025 によると Discard と NopCloser はたまたま io/ioutil パッケージに含まれてしまった、とのことです。ReadAll も本来であれば Reader や Writer のヘルパー関数が io パッケージで提供されていることにならって io パッケージに含まれる機能でしたが、bytes.Buffer にアクセスする実装となっていており、bytes パッケージが os パッケージに依存する関係で io/ioutil パッケージに追いやられました。
しかしながら ReadAll は bytes.Buffer を使う必要がないため、Go1.16の ReadAll の実装は bytes パッケージを使わない実装になっています。
- Go1.15の
ioutil.ReadAllの実装(io/ioutil/ioutil.go)
// readAll reads from r until an error or EOF and returns the data it read |
- Go1.16の
io.ReadAllの実装(io/io.go)
// ReadAll reads from r until an error or EOF and returns the data it read. |
バイトスライスの容量を拡張するときに一旦 byte 型のゼロ値0をバイトスライスにappendして容量を拡張し、長さは元のスライスの長さに戻すことでスライスの容量だけ予め拡張するオシャレな実装になっています。
io/ioutil パッケージからは io.ReadAll に委譲するように実装されています。
- Go1.16の
ioutil.ReadAllの実装(io/ioutil/ioutil.go)
// ReadAll reads from r until an error or EOF and returns the data it read. |
os パッケージに移動する関数
os パッケージに移行する関数は以下の5つです。io/ioutil パッケージに含まれていた下記の関数はOSファイルシステムのヘルパー関数です。
ReadDirReadFileTempDir->MkdirTemp(リネーム)TempFile->CreateTemp(リネーム)WriteFile
リネーム
ioutil の2つの関数がリネームになっています。TempDir 関数は既に os パッケージに存在します。os.TempDir 関数はOSのデフォルトの一時ディレクトリを返却するAPIです。ioutil パッケージの TempFile は今回移行した os パッケージでは MkdirTemp という関数になっています。また MkdirTemp との命名の整合をとるために ioutil パッケージに存在していた TempFile は CreateTemp という命名になっています。
シグネチャ
ReadDir は返却するシグネチャが更新されています。移行後の ReadDir は fs.FileInfo ではなく os.DirEntry を返却するようになっています。
- Go1.16の
os.ReadDirの実装(os/dir.go)
// ReadDir reads the named directory, |
- Go1.16の
ioutil.ReadDirの実装(io/ioutil/ioutil.go)
// ReadDir reads the directory named by dirname and returns |
"deprecated" とは
"deprecated" とあるため、将来的に ioutil パッケージを使っているプログラムが動作しなくなるのでは? と思う方もいるかもしれません。"deprecated" が何を意味するのか Russ Cox 氏がツイートしています。
Seeing discussion of io/ioutil deprecation in various places.
— Russ Cox (@_rsc) January 19, 2021
To be clear: in Go, “deprecated” means only “there is a better, preferred way.” It does not mean “will break in the future.”
Today’s programs using ioutil.ReadFile and friends will keep working forever. #golang
すわなち「より良い、好ましい方法がある」という意味で「将来的に壊れる」という意味ではない、ということです。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 |
Diffを確認できます。-diff オプションを除けば、ファイルを直接更新できます。
$ go tool fix -diff main.go |
ioutil パッケージの go fix に関して #42026 で言及されています。残念ながら #32816 のプロポーザルには含まれない だろう、とコメントしています。将来的に go fix コマンド一発で既存の ioutil パッケージを使っているAPIから io パッケージや os パッケージのAPIへ移行ができると嬉しいですね。