The Gopher character is based on the Go mascot designed by Renée French
はじめに
TIG 真野です。Go1.22連載の3本目です。
Go 1.22のマイナーアップデートのうち、ファイルなどの入出力に関連しそうな archive/tar
・archive/zip
・bufio
・io
を取り上げて紹介します。
アップデートサマリ
- archive/zip に
Writer.AddFS
が追加された #54898 - archive/tar に
Writer.AddFS
が追加された #58000 - bufio の Scannerが、
SplitFunc
がErrFinalToken
を返すときに即時停するようになった。従来は[]byte{}
を返していた #56381 - io の
SectionReader.Outer()
メソッドが追加された #61870
archive/zip #54898
archive/zipパッケージにWriter.AddFS というメソッドが追加されました。処理としては、FS、つまりファイルシステムを入力として、ルートからディレクトリツリーを辿ってフォルダ構成を維持しながら全ファイルをzip化します。便利ですね。
これが登場する以前は、Stack Overflowなど複数の記事でいくつか実装例を参考にしながら各自が実装していたようで、揺れていたり実装ミスが発生したようです。zip.NewWriter()でzipに追加したいファイルを1つ1つ追加する必要がありました。
Stack Overflowの例もFileWalkerなどを使って(それなりの量を)実装する必要があります。また、w.Create()の前に書いてあるコメント通り、指定されたパスが相対パスにする必要があったり、Windowsでも動作するようにするためには、一工夫がさらに必要です。
package main |
これが次のように書き換わります。
package main |
とても楽ですし、直感的ですね! ちなみに、空フォルダはzip化されないようです。
また、何かしらのファイルを除去したいなどのフィルター処理をしたい場合は、それを行う fs.FS
を作成して回避するといった考えのようです。
fs.FS
を引数に取るということは、別の応用も効かせられます。jszwec/s3fs はS3の指定されたバケットに対してfs.FS
インタフェースを満たすライブラリです。例えばこれを用いると、S3バケットがそのままzip化されます。
package main |
ビルディングブロック的に、zip化ができるようになったのは画期的だと思います。
【注意】上記のコードはS3バケットまるごとダウンロードするので、バケットのデータ量によっては利用を控えたほうが無難です。また、s3fs側の実装とAddFS()の組み合わせが悪いのか、S3に空フォルダオブジェクト(キー名が/
で終わるオブエジェクト)が含まれる場合は、上手くzip化されないようです(実行時エラーとなります)。ご注意ください。
archive/tar #58000
archive/tarパッケージにWriter.AddFS というメソッドが追加されました。背景や内容については、archive/zip
と全く同じでしたので割愛します。
bufio #56381
Scannerが、bufio.SplitFunc を受け取り、 ErrFinalToken
を返した場合は停止するようになりました。従来は []byte
を返していました。
まずScannerにはSplit() という関数があり、Split()
は SplitFunc
を引数に取ります。
func (s *Scanner) Split(split SplitFunc) |
例としてIssueにあったScannerの実装を上げます。
func main() { |
上記で、scanner.Scan() ですが、inputは1行ですが、ループはカンマごとにSplitFunc() で分割され、またSTOPという文字列で停止するために2回ループが実行されるのが想定だと思います。
go1.21以前では、これが3回実行されていましたが、go1.22以降では2回の実行となります。
go1.21以前:
Got a token "1" |
go1.22以降:
Got a token "1" |
変更理由は、このデータを取りたいケースは存在しないだろうということで、どちらかといえばあるべき動きに訂正されたようです。
io(#61870)
io.SectionReader
に以下のメソッドが追加されました。
func (s *SectionReader) Outer() (r ReaderAt, off int64, n int64) |
SectionReader自体はRead(), Seek(), ReadAt() を実装する、入力を指定された オフセット~長さに区切ったReaderです。GoDocのExampleを見ると何をするようなものか一目瞭然です。
package main |
これに Outer()
を追加します。
func main() { |
実行すると、引数で渡したstrings.Reader, offset=5, length=17
が取得できます。
さて、機能はわかったところで、これが追加された理由です。io
側のコメントを追っていくと、net: support zero-copy send on TCPConn when reading from File via SectionReader #61727がモチベーションのようです。
#61727
の内容は私が理解しきれた範囲だと以下です(補足、訂正大歓迎です)
net.TCPConn
でファイル送信する場合に、ゼロコピーになるのは現在、LimitedReader
のみ- https://github.com/golang/go/blob/cc85462b3d23193e4861813ea85e254cfe372403/src/net/sendfile_linux.go#L23-L28 の実装を見ると、 handled=trueの場合はゼロコピーとなる。
- そうじゃない場合は、netパッケージのgenericReadFromが呼ばれる
SectionReader
もゼロコピー対応したい- 対応できると、FD(File descriptor)を同時に使用して同じファイルを複数の TCP 接続に送信できるようになり、他のユースケース (範囲リクエストなど) もサポートできる
- 今でもそれはできるが、システムコールやメモリ割り当てが発生してしまう(≒genericReadFromが呼ばれる)
この対応が入れば、GoのHTTPサーバの応答性能がさらに上がりそう、というのがわかります。今後に期待ですね。
まとめ
派手さはないですが、こういった細かいアップデートを確認していくと、Stack Overflowなどでコミュニティ側が混乱していそうな点を標準パッケージ側で吸収しGoアプリケーションとしての品質向上に努めたり、性能観点など、確実にGoが良くなっているのが感じられます。
個人的には、fs.FSをインタフェースにzip化できるのは、便利で応用力が高くて良い設計だな感嘆しました。私が設計者なら、普通にディレクリパスを渡して、あるフォルダごとzip化するようなインタフェースを考えてしまいそうです。
ライブラリのAPI設計の勉強にもなり学びでした。次のバージョンでもこのブログ連載に参加しようと思います。