フューチャー技術ブログ

Go 1.23リリース連載 os.CopyFS & path/filepath

はじめに

Go1.23連載の6本目です。

Go1.23のos.CopyFSの追加、path/filepath パッケージの更新について解説します。

更新内容

os.CopyFS の追加

プロポーサルは#62484です。

io.fs/FSをローカルにコピーできるようになりました。

バージョン1.22まではディレクトリのコピーなどは、filepath.Walkなどを使い再帰的にコピーを行うか、外部ライブラリなどを利用する必要がありました。

1.23では標準パッケージを利用しつつ簡単な実装でディレクトリコピーをできるようになりました。

main.go
package main

import (
"fmt"
"os"
)

func main() {
err := os.CopyFS("to_dir", os.DirFS("from_dir"))
if err != nil {
panic(err) // サンプルのためpanicで実装しています。
}
fmt.Println("Copied")
}

os.CopyFSの第2引数は io/fs.FSのinterfaceに対応していれば良いので、標準パッケージではあれば go:embed で埋め込んだファイルリストをコピー可能、is/fs.FSに準拠した外部ライブラリを利用すればS3やメモリ上にある仮想ファイルシステムをローカルにコピーすることも可能です。

embedos.CopyFS利用のサンプルです。

main.go
package main

import (
+ "embed"
"fmt"
"os"
)

+ //go:embed from_dir/*
+ var static embed.FS
+
func main() {
- err := os.CopyFS("to_dir", os.DirFS("from_dir"))
+ err := os.CopyFS("to_dir", static)
if err != nil {
panic(err) // サンプルのためpanicで実装しています。
}
fmt.Println("Copied")
}

io.fs/FSOpenしか用意されていないためio.fs/FS=>ローカル方向へのコピーのみである点はご注意ください。

ちなみに後ほど紹介するLocalizeos.CopyFS内部で利用されています。
(こちらで利用されているのはinternalパッケージですが)

プロポーサルは#63703です。

os.Readlinkの実装がドキュメントの記載である

Readlink returns the destination of the named symbolic link.

とは異なり、readlinkコマンドの挙動と一致させるような複雑な実装になっていました。

その結果深刻なバグ(#39786, #40176)が発生しおり、filepath.EvalSymlinks も内部でos.Readlinkを利用しているため影響を受けていました。

今回の変更により以下のように変更されてます。

対象 Before After
os.Lstat symlinksとmount pointsを両方考慮 symlinksのみ考慮
os.Readlink windows.GetFinalPathNameByHandleの結果から
symlinksとmount pointsを両方考慮
シンボリックリンクのターゲット
(IO_REPARSE_TAG_SYMLINK)を利用し返却

Window上でシンボリックを張り1.23rc2と1.22.5の違いを確認しましたが、単純なパターンでは結果は変わりませんでした。

確認ソースコード
シンボリックリンク準備コマンド
cmd
C:\gotest>type nul > ori.txt
C:\gotest>mkdir oridir
C:\gotest>mklink sym.txt ori.txt
sym.txt <<===>> ori.txt のシンボリック リンクが作成されました
C:\gotest>mklink /D symdir oridir
symdir <<===>> oridir のシンボリック リンクが作成されました
C:\gotest>
main.go
package main

import (
"fmt"
"os"
"path/filepath"
)

func main() {
fmt.Println(os.Readlink(`C:\gotest\ori.txt`)) // readlink C:\gotest\ori.txt: The file or directory is not a reparse point.
fmt.Println(os.Readlink(`C:\gotest\sym.txt`)) // ori.txt <nil>
fmt.Println(filepath.EvalSymlinks(`C:\gotest\sym.txt`)) // C:\gotest\ori.txt <nil>
fmt.Println(filepath.EvalSymlinks(`C:\gotest\ori.txt`)) // C:\gotest\ori.txt <nil>
fmt.Println(os.Readlink(`C:\gotest\oridir`)) // readlink C:\gotest\oridir: The file or directory is not a reparse point.
fmt.Println(os.Readlink(`C:\gotest\symdir`)) // oridir <nil>
fmt.Println(filepath.EvalSymlinks(`C:\gotest\oridir`)) // C:\gotest\oridir <nil>
fmt.Println(filepath.EvalSymlinks(`C:\gotest\symdir`)) // C:\gotest\oridir <nil>
}

filepath.Localize の追加

プロポーサルは#57151です。

filepath.Localizeはスラッシュ区切りのパスをOSに合わせ安全に変換する機能です。

返却される値、エラーの有無はテストケース見ると分かりやすいです。

具体例をいくつか上げます。

入力値 返却値
(Windows)
返却値
(UNIX)
a a a
. . .
a/b/c a\b\c a/b/c
a/ (error) (error)
a/./b (error) (error)
a/.. (error) (error)
a\b:c (error) a\b:c
(\,:を区切りとして扱わない)
c: (error) c:
/a (error) (error)

. (.のみを除く)や..が含まれていた場合、先頭末尾にスラッシュが入った場合にもエラーになるためかなり厳しくチェックすることが可能です。

まとめ

今回はos, path/filepathを中心に3件のアップデートを紹介しました。

os.CopyFSはローカルに対するコピーの利便性向上、後半2つは安全性、安定性向上のアップデートでした。

明日は大江さんの keep-alive,Cookie です。