Skip to content

Instantly share code, notes, and snippets.

@tenntenn
Last active August 31, 2020 09:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tenntenn/fe8995c347a5e1000832d3c6942f1fbe to your computer and use it in GitHub Desktop.
Save tenntenn/fe8995c347a5e1000832d3c6942f1fbe to your computer and use it in GitHub Desktop.
Draft designを読む

Bug-resistant build constraints

早いとGo1.17で入る可能性がある。

// +build linux windows
// +build amd64

これは(linux || windows) && amd64の意味。 同じ行に書いたものがORで別の行に書くとANDになる。

最初に使えたのは、GOOSGOARCHまたはGOOS/GOARCHのみ(2011/09)。 それから、cgonocgoが追加された(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

  1. //go:buildのサポートを追加する(// +buildより優先される)
  2. gofmt// +buildコメントの上に//go:buildを自動追加する
  3. gofmtはBuild Constraintの場所を適切な場所に移動する
  4. 間違っている// +buildコメントを修正する
  5. 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の可能性がある。

io/fs file system interface for Go

ファイルをツリー構造で扱うような抽象化は色んな所で応用が効く。 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)
}

Filefsパッケージで定義されるインタフェース。

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.Filefs.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/ziposパッケージでは、io/fsパッケージで提供しているインタフェースを実装していて、html/templatenet/httptext/templateではio/fsパッケージの型を使っている。

https://youtu.be/yx7lmuwUNv8?t=1553

osパッケージでは、FileInfoFileModePathError、エラーなどを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:embed directive comment

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`から取れる。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment