早いとGo1.17で入る可能性がある。
// +build linux windows
// +build amd64
これは(linux || windows) && amd64
の意味。
同じ行に書いたものがORで別の行に書くとANDになる。
最初に使えたのは、GOOS
、GOARCH
またはGOOS/GOARCH
のみ(2011/09)。
それから、cgo
とnocgo
が追加された(2011/12)。
そして、appengine
などのカスタムビルドタグや!
が追加された(2012/01)。
ANDのスラッシュがカンマになった。
// +build linux,!cgo appengine
は(linux&&!cgo)||appengine
を意味する。
// +build !go1.15
のようなバージョンを表すビルドタグが追加された(2013/03)。
これまでのBuild Constraintには複雑で問題があった。
- 複数行に分ければAND
- スペースでOR
- カンマでAND
- !でビルドタグの否定
難しい例
// +build linux darwin
// +build amd64 arm64 mips64x ppc64x
これは、(linux||darwin)&&(amd64||arm64||mips64x||ppc64x)
を表す。
これのフォールバックファイルを作りたい場合は
// +build !linux,!darwin
// +build !amd64,!arm64,!mips64x,!ppc64x
のようにすれば良さそうだが間違い。
(!linux && !darwin) && (!amd64 && !arm64 && !mips64x &&!ppc64x)
になってしまう。
Linuxの32ビットとかが含まれない。
https://youtu.be/AgR_mdC4Rs4?t=224
// +build
コメントは他のコメントと別れている必要がある。
/* */
コメントを先頭に書いてもだめ。
実際に調べた結果いくつか問題のあるBuild constraintがあった。
https://youtu.be/AgR_mdC4Rs4?t=574
//go:build
を導入する。
Build Constraintに式を使えるようにする。
BuildLine = "//go:build" Expr
Expr = OrExpr
OrExpr = AndExpr { "||" AndExpr }
AndExpr = UnaryExpr { "&&" UnaryExpr }
UnaryExpr = "!" UnaryExpr | "(" Expr ")" | tag
tag = tag_letter { tag_letter }
tag_letter = unicode_letter | unicode_digit | "_" | "."
//go:build
の前にコメントを置くことを許可するようになる。
単に、pacakge
句より前におけばOK。
Go1.15から//xxx:xxx
みたいなコメントはDocとして扱われないようになった。
具体的には*ast.CommentGroup
型のTextメソッドが返す文字列から除外されるようになった。この変更により、//lint:ignore
コメントも取れなくなった。
なお、//go:build
コメントは1行で書く必要がある。
リリースは3つに分けて行われる。
- Go 1.(N-1)は変更のための準備
- Go 1.Nで
//go:build
に変更される - Go 1.(N+1)で変更後の後始末
移行中は// +build
コメントも//go:build
コメントも必要。
準備: Go 1.(N-1)
goツールは特に変わらず// +build
を使う。
しかし、//go:build
を認識して、// +build
と共に//go:build
を使ってない場合にはエラーにする。
変更:Go 1.N
//go:build
のサポートを追加する(// +build
より優先される)gofmt
は// +build
コメントの上に//go:build
を自動追加するgofmt
はBuild Constraintの場所を適切な場所に移動する- 間違っている
// +build
コメントを修正する go vet
は//go:build
コメントと// +build
コメントが表すBuild Constraintが一致しない場合に指摘するようになる
後処理: Go 1.(N+1)
go fix
が// +build
を取り除き、同等の//go:build
を追加する。
削除はGo Modulesモードでかつ、go.modに記述していあるGoのバージョンがgo 1.N
以上の時のみ。GOPATHモードの場合は何もおきない。
最速でNは17の可能性がある。
ファイルをツリー構造で扱うような抽象化は色んな所で応用が効く。 REST APIとかも。
Go Docの内部で使われている。 go-bindataでもファイルシステム的な機能がある。
go-bindataではRestoreAsset
を使って一度OSのファイルシステムに書き出してから、
他のパッケージが使うようになっている。直接できた方が便利だよねっていう話。
https://youtu.be/yx7lmuwUNv8?t=556
net/http
パッケージにもファイルシステムを扱うための型が存在する。
http
パッケージのインタフェースはSeek
メソッドとかあって実装しづらい。
Readdir
メソッドとかも必要のないパターンもある。
ネガティブなフィードバックが来ない限りはGo1.16でリリースされる可能性がある。
io/fs
パッケージを作り、FS
インタフェースとFile
インタフェースを定義する。
以下のパッケージにもマイナーチェンジが入る。
archive/zip
html/template
net/http
os
text/template
FS
インタフェースは非常にシンプルで最低限の機能を定義している。
type FS interface {
Open(name string) (File, error)
}
File
はfs
パッケージで定義されるインタフェース。
Open
メソッドの引数はパスで、スラッシュ区切り。
Windowsでもスラッシュ区切りで、.
や..
などは使えない。
ただし、ルートは.
で表し、Open(".")
とは書ける。
https://youtu.be/yx7lmuwUNv8?t=839
File
インタフェースも最小限の機能を定義している。
type File interface {
Stat() (os.FileInfo, error)
Read([]byte) (int, error)
Close() error
}
fs.File
インタフェースの3つのメソッドは*os.File
型も持つ。
https://youtu.be/yx7lmuwUNv8?t=882
ディレクトリとして読み込むために、ReadDirFile
インタフェースも提供している。
File
インタフェースに加えて、ReadDir
メソッドを提供している。
type ReadDirFile interface {
File
ReadDir(n int) ([]os.FileInfo, error)
}
io
パッケージのio.ReadWriter
と同じように、fs.File
をfs.ReadDirFile
に埋め込んでいる。
https://youtu.be/yx7lmuwUNv8?t=900
ReadFileFS
インタフェースはFS
インタフェースにReadFile
メソッドを追加したもの。
type ReadFileFS interface {
FS
ReadFile(name string) ([]byte, error)
}
ReadFile
メソッドは直接ファイルの中身を読み込むためのメソッド。
ヘルパーとしてReadFile
関数を用意。
io.StringWriter
インタフェースに対する、io.WriteString
関数の関係と同じ。
しかし、io.StringWriter
にはWrite
インタフェースは埋め込まれていないので注意。
WriteString
の場合は、WriteString
が必要なケースはもはやWrite
メソッドを必要ではないので、埋め込む必要はない。
func ReadFile(fsys FS, name string) ([]byte, error) {
if fsys, ok := fsys.(ReadFileFS); ok {
return fsys.ReadFile(name)
}
file, err := fsys.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
return io.ReadAll(file)
}
https://youtu.be/yx7lmuwUNv8?t=965
StatFS
インタフェースはFS
インタフェースにStat
メソッドを追加したもの。
ヘルパー関数として、Stat
関数がある。
type StatFS interface {
FS
Stat(name string) (os.FileInfo, error)
}
func Stat(fsys FS, name string) (os.FileInfo, error) {
if fsys, ok := fsys.(StatFS); ok {
return fsys.Stat(name)
}
file, err := fsys.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
return file.Stat()
}
https://youtu.be/yx7lmuwUNv8?t=1130
ReadDirFS
インタフェースはFS
インタフェースにReadDir
を追加したもの。
ヘルパー関数としてReadDir
関数を用意。
type ReadDirFS interface {
FS
ReadDir(name string) ([]os.FileInfo, error)
}
func ReadDir(fsys FS, name string) ([]os.FileInfo, error)
https://youtu.be/yx7lmuwUNv8?t=1170
io/fs
パッケージはfilepath.Walk
関数に似たfs.Walk
関数をトップレベルで提供するが、対応する拡張されたインタフェースは提供しない。
Walk
は実装は面倒なのにファイルシステムごとの最適化を行う余地があまりない。
https://youtu.be/yx7lmuwUNv8?t=1188
GlobFS
インタフェースはFS
パッケージにGlob
メソッドを追加したインタフェース。
Glob
メソッドはfilepath.Glob
のようなもの。
ヘルパー関数でGlob
関数も提供。
type GlobFS interface {
FS
Glob(pattern string) ([]string, error)
}
func Glob(fsys FS, pattern string) ([]string, error)
Walk
ほどではないが、実装は面倒だが、リモートのファイルシステムで*.go
を検索するような場合に、ファイルシステムによって最適化の余地があるのでインタフェースを提供している。
https://youtu.be/yx7lmuwUNv8?t=1269
サードパーティでFS
インタフェースやFile
インタフェースの拡張インタフェースを提供するのもよい。例えば、FS
インタフェースにRename
メソッドを追加したRenameFS
インタフェースを考えてみる。
type RenameFS interface {
fs.FS
Rename(oldpath, newpath string) error
}
func Rename(fsys fs.FS, oldpath, newpath string) error {
if fsys, ok := fsys.(fs.RenameFS); ok {
return fsys.Rename(oldpath, newpath)
}
return fmt.Errorf("rename %s %s: operation not supported", oldpath, newpath)
}
OpenFileFS
も同様。
type OpenFileFS interface {
fs.FS
OpenFile(name string, flag int, perm os.FileMode) (fs.File, error)
}
func OpenFile(fsys FS, name string, flag int, perm os.FileMode) (fs.File, error) {
if fsys, ok := fsys.(OpenFileFS); ok {
return fsys.OpenFile(name, flag, perm)
}
if flag == os.O_RDONLY {
return fs.Open(name)
}
return fmt.Errorf("open %s: operation not supported", name)
}
io/fs
パッケージでは最低限のReadOnlyなファイルシステムを提供している。
https://youtu.be/yx7lmuwUNv8?t=1373
archive/zip
とos
パッケージでは、io/fs
パッケージで提供しているインタフェースを実装していて、html/template
、net/http
、text/template
ではio/fs
パッケージの型を使っている。
https://youtu.be/yx7lmuwUNv8?t=1553
os
パッケージでは、FileInfo
やFileMode
、PathError
、エラーなどをfs
パッケージから型エイリアスや値のコピーを行うように変更している。
io/fs
に依存するパッケージがos
パッケージにも依存しないようにしている。
https://youtu.be/yx7lmuwUNv8?t=1623
os
パッケージに、DirFS
関数を追加。
指定したディレクトリをルートに取るようなファイルシステムを返すような関数。
package os
// DirFS returns an fs.FS implementation that
// presents the files in the subtree rooted at dir.
func DirFS(dir string) fs.FS
https://youtu.be/yx7lmuwUNv8?t=1727
text/template
パッケージとhtml/template
パッケージに、ParseFS
関数と*Template
型にParseFS
関数を追加する。指定したファイルシステムにおいて、ParseFiles
メソッドとParseGlob
メソッドをあわせたようなメソッド。
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
https://youtu.be/yx7lmuwUNv8?t=1808
net/http
パッケージには独自のファイルシステムのインタフェースがある。
FileServer
関数に似たHandleFS
関数を追加。
HandlerFS
の引数のファイルシステムがOpen
で返すファイルはio.Seeker
を実装していないと、Rangeアクセスが掛けられたときに500を返す。Go Docに書かれる予定。
func HandlerFS(fsys fs.FS) Handler
https://youtu.be/yx7lmuwUNv8?t=1866
archive/zip
パッケージの*Reader
型にOpen
メソッドを追加。
func (r *Reader) Open(name string) (fs.File, error)
こうすることによって、*zip.Reader
型はfs.FS
インタフェースを実装することになり、template.ParseFS
関数に渡せるようになる。
zfs, _ := zip.OpenReader("tempaltes.zip")
t, _ := template.ParseFS(zfs, "*.tmpl")
zip.OpenReader
関数で返すファイルシステムのファイルはSeek
メソッドを実装していない。
なので、サードパーティ製でCachedFS
みたいな関数を作ってもインメモリキャッシュをして、Seekメソッドを実現してもよい。
func CachedFS(fsys fs.FS) fs.FS
http.Handle("/", http.HandleFS(CachedFS(fs)))
archive/tar
は変更なし。
https://youtu.be/yx7lmuwUNv8?t=2064
zipで見たようにfs.FSを実装してレイヤーをかぶせることもできる。 例えば、暗号化レイヤーをかぶせることもできそう。
io/fs
パッケージ//go:embed
ディレクティブコメントでも使われている。
go-bindataとかあった。 いくつか問題があった。 事前に走らせて、コード上に生成する必要があった。 go generateの制約だが、go generateは
- シンプルであること(生成されたコードがあれば別にツールの実行が不要)
- ビルドの再現性が取れること
- 安全性(知らないツールが勝手に走るのはよくない)
という観点から自動では走らない。
//go:embed
は裏で勝手に自動生成してビルドをやってくれる。
https://youtu.be/rmS-oWcBZaI?t=371
httpパッケージとかとも相性よく使えて、http.HandleFSとか使える。
https://youtu.be/rmS-oWcBZaI?t=458
プロポーザルじゃないけど、すべてがスムーズにいくとGo1.16に入る可能性もある。 フィードバックの結果次第。
go/build
パッケージとgolang.org/x/tools/go/packages/
パッケージが影響うける。
ディレクトリやワイルドカードも使える
//go:embed *.txt images
var content embed.Files
.
とか..
とか、空のディレクトリや他のモジュールのディレクトリは使えない。
embed
パッケージはfs
パッケージに依存している。
package embed
type Files struct {/*...*/}
func (f Files) Open(name string) (fs.File, error) {}
func (f Files) ReadFile(name string) ([]byte, error) {}
template.ParseFS
が追加される。
https://youtu.be/rmS-oWcBZaI?t=695
`go list`コマンドで埋め込まれているパータンが取得できる。
https://youtu.be/rmS-oWcBZaI?t=730
テストでも使えそう。
`packages.package`から取れる。