Slide 1

Slide 1 text

1 embedパッケージを理解する Gopher’s Gathering 2025/01/18 @task4233

Slide 2

Slide 2 text

2 所属 ・Mercariのバックエンドエンジニア ・認証認可基盤まわりの開発をしています 興味 ・バックエンドとWebセキュリティ ・好きなパッケージはioパッケージ 自己紹介: @task4233

Slide 3

Slide 3 text

3 アイスブレイクしますか

Slide 4

Slide 4 text

4 embedパッケージ 使ったことある人〜?👀

Slide 5

Slide 5 text

5 embedパッケージ関連の 実装を読んだことある人〜?👀

Slide 6

Slide 6 text

6 想定対象者 ・Go言語の初心者から経験豊富な方まで (ref: イベント概要) 発表の目的 ・embedパッケージの概要を理解してもらうこと ・//go:embed ディレクティブで指定されたファイルが  バイナリへ埋め込まれるまでの流れを掴んでもらうこと 注意 ・非本質的な説明を省きます  ・全て話すには時間がかかりすぎるので  ・後ほどブログで詳細な説明を出す予定 ・Go1.23.5の情報をベースに扱います 発表に際して

Slide 7

Slide 7 text

7 embedパッケージの概要 おしながき ファイルが埋め込まれるまでの流れ 02 03 01 まとめ Appendix: 関連issueおよび参考文献 04

Slide 8

Slide 8 text

8 コンパイル時にGoバイナリへ「埋め込まれた」ファイルに関するパッケージ ・埋め込むファイルは //go:embed 対象パス で変数ごとに指定 ・Read-Onlyなファイルの参照が可能 ・Go1.16で標準パッケージとして追加: https://pkg.go.dev/embed  ・Proposal: https://github.com/golang/go/issues/35950  ・Draft: https://github.com/golang/go/issues/41191 embedパッケージの概要 Go1.16 リリースノートの抜粋

Slide 9

Slide 9 text

9 1. 外部ファイルをGoバイナリに埋め込んで管理/配布できる ・パス指定が原因でファイル参照できないバグを防げる ・埋め込まれているため、データを編集しても実ファイルに反映されない 2. 実行時にディスクI/Oが発生しない ・データがメモリ上に存在するため ・osパッケージでのアクセスのようにディスクI/Oが発生しない osパッケージと比べた時の優位性 各パッケージのReadFileのベンチマーク結果 ファイルサイズ [B] os [ns/op] embed.FS [ns/op] os / embed.FS 64 26321 109.6 240.1 2048 26343 290.1 90.81 16777216 671421 356865 1.882

Slide 10

Slide 10 text

10 1. 単一ファイルの埋め込み: string、[]byte型の変数へ 2. 複数ファイルの埋め込み: embed.FS型の変数へ 2種類の埋め込み方法 import _ "embed" //go:embed assets/hello.txt var hello string //go:embed assets/hello.txt var helloBytes []byte func main() { println(hello) println(helloBytes) } 1: 単一ファイルの埋め込み import "embed" //go:embed assets/* var assets embed.FS func main() { var b []byte // 説明用にエラーハンドリングは省略 b, _ = assets.ReadFile("hello.txt") println(b) } 2: 複数ファイルの埋め込み ※単一ファイルのみの指定も可能

Slide 11

Slide 11 text

11 1. 単一ファイルの埋め込み: string、[]byte型の変数へ 2. 複数ファイルの埋め込み: embed.FS型の変数へ embedパッケージの 2つの利用例 import _ "embed" //go:embed assets/hello.txt var hello string //go:embed assets/hello.txt var helloBytes []byte func main() { println(hello) println(helloBytes) } 1: 単一ファイルの埋め込み import "embed" //go:embed assets/* var assets embed.FS func main() { var b []byte // 説明用にエラーハンドリングは省略 b, _ = assets.ReadFile("hello.txt") println(b) } 2: 複数ファイルの埋め込み ※単一ファイルのみの指定も可能 string 型、[]byte型の変数へ 単一ファイルを埋め込む

Slide 12

Slide 12 text

12 1. 単一ファイルの埋め込み: string、[]byte型の変数へ 2. 複数ファイルの埋め込み: embed.FS型の変数へ embedパッケージの 2つの利用例 import _ "embed" //go:embed assets/hello.txt var hello string //go:embed assets/hello.txt var helloBytes []byte func main() { println(hello) println(helloBytes) } 1: 単一ファイルの埋め込み import "embed" //go:embed assets/* var assets embed.FS func main() { var b []byte // 説明用にエラーハンドリングは省略 b, _ = assets.ReadFile("hello.txt") println(b) } 2: 複数ファイルの埋め込み ※単一ファイルのみの指定も可能 embed.FS 型の変数へ 複数ファイルを埋め込む

Slide 13

Slide 13 text

13 埋め込みファイルの指定方法 // import _ "embed" import "embed" // 単一ファイルの埋め込み //go:embed assets/hello.txt //go:embed assets/hello.txt //go:embed assets/h*.*t // コメントと空白行を間に書いても OK var hello string // 複数ファイルの埋め込み //go:embed main.go go.mod //go:embed "secret.txt" //go:embed `quoted.txt` var assets embed.FS embedパッケージのimportが必要 ・string 型、 []byte 型への埋め込み  ではblank importをする必要がある blank importをやめるためのProposalも ・https://github.com/golang/go/issues/62417

Slide 14

Slide 14 text

14 埋め込みファイルの指定方法 // import _ "embed" import "embed" // 単一ファイルの埋め込み //go:embed assets/hello.txt //go:embed assets/hello.txt //go:embed assets/h*.*t // コメントと空白行を間に書いても OK var hello string // 複数ファイルの埋め込み //go:embed main.go go.mod //go:embed "secret.txt" //go:embed `quoted.txt` var assets embed.FS //go:embed ディレクティブでファイル指定 ・ go:embed の後にスペースを挟んで  パスを書く 変な書き方も出来る ・間の // 形式のコメントと空白行 ・複数回の同じディレクティブ ・ワイルドカード(*)の利用

Slide 15

Slide 15 text

15 埋め込みファイルの指定方法 // import _ "embed" import "embed" // 単一ファイルの埋め込み //go:embed assets/hello.txt //go:embed assets/hello.txt //go:embed assets/h*.*t // コメントと空白行を間に書いても OK var hello string // 複数ファイルの埋め込み //go:embed main.go go.mod //go:embed "secret.txt" //go:embed `quoted.txt` var assets embed.FS //go:embed ディレクティブでファイル指定 ・ go:embed の後にスペースを挟む 変な書き方 ・間にはコメントと空白行を入れても良い ・同じディレクティブを2回書いても良い ・ワイルドカードを利用しても良い 単一ファイルの埋め込み ・//go:embed assets/hello.txt  を1行書いた時と同じ意味 ※assetsディレクトリ内で h*.*t に マッチするファイルが1つのみの場合

Slide 16

Slide 16 text

16 埋め込みファイルの指定方法 // import _ "embed" import "embed" // 単一ファイルの埋め込み //go:embed assets/hello.txt //go:embed assets/hello.txt //go:embed assets/h*.*t // コメントと空白行を間に書いても OK var hello string // 複数ファイルの埋め込み //go:embed main.go go.mod //go:embed "secret.txt" //go:embed `quoted.txt` var assets embed.FS 他のファイル指定方法 ・複数行のディレクティブ ・スペース区切りのパス ・ダブルクオート(")で囲まれたパス ・バッククオート(`)で囲まれたパス

Slide 17

Slide 17 text

17 下記の記号を含むパスは埋め込めない ・ *, <, >, ?, `, ’, |, /. \, : ディレクトリ内のドット ( . )およびアンダースコア ( _ )始まりのファイルは 基本的に無視される ・assets/.env など ・指定したい場合はパスの前に all: プレフィックスをつける  例) //go:embed all:assets/* 対象ファイルが 1つも存在しないパスは //go:embed ディレクティブで 指定できない ・上記の無視されるファイルのみで構成されたファイルツリー ・空ディレクトリを指定したパス 埋め込みファイル指定における注意点

Slide 18

Slide 18 text

18 io/fsはファイルシステムを抽象化したパッケージ ・https://pkg.go.dev/io/fs ・embedパッケージと同様にGo 1.16で標準パッケージ入りした 👏 embed.FSはio/fsのインタフェースを実装している type FS interface { Open(name string) (File, error) } type ReadDirFS interface { FS ReadDir(name string) ([]DirEntry, error) } type ReadFileFS interface { FS ReadFile(name string) ([]byte, error) } Go1.23.5で実装されているインタフェース

Slide 19

Slide 19 text

19 利用者はファイルシステムの実態を意識せずに利用することが出来る ・Windows/MacOS/Linuxのファイルシステムでも良い ・embedパッケージで埋め込まれたファイル群でも良い ← New! io.fsのインタフェースを引数に取るパッケージでも同様に利用できる ・net/httpのhttp.FileServer ・html/template、text/templateのParseFS embed.FSがio/fs.FSを実装している嬉しさ //go:embed assets/* var assets embed.FS func main() { http.Handle("/assets/", http.FileServer(http.FS(assets))) http.ListenAndServe(":8080", nil) } embedパッケージで静的リソースをサーブする例

Slide 20

Slide 20 text

20 ・$ go list コマンドでembedパッケージの関連情報を取得できる 埋め込まれた対象ファイルの確認 コマンド 実行結果の例 列挙されるもの $ go list -f '{{.EmbedPatterns}}' [assets/*] //go:embed ディレクティブで 指定されたパターン (testは対象外) $ go list -f '{{.EmbedFiles}}' [assets/icon.png assets/icon.jpg] 埋め込み対象となったファイル一覧 (testは対象外) $ go list -f '{{.XTestEmbedPatterns}}' [assets/*] //go:embed ディレクティブで 指定されたパターン (testのみ) 関連情報の取得用コマンド一覧

Slide 21

Slide 21 text

21 -embedcfgオプション 埋め込み対象のファイル情報を渡すオプション ・裏でコンパイル時に内部的に生成して参照している(Go workspaceの機能) ・設定はJSON形式で指定 ・JSONを吐き出す実装がある JSONを吐き出す機能 build時の生成例

Slide 22

Slide 22 text

22 どのように実現されているのか?

Slide 23

Slide 23 text

23 embedパッケージに関連するコードを読む ・cmd/compileパッケージのmain関数から読むだけ ・パッケージごとの依存関係は右図の通り(リンク) ・経験者は割と読み慣れているはずだが、  初心者には全く優しくない ・そこで、本質部分だけを抜粋して紹介する形に

Slide 24

Slide 24 text

24 Goコンパイラ処理は以下の3ステージに大別される ・フロントエンド : ソースコードを解析して中間表現(IR: コンパイラAST)を 構築するステージ ・ミドルエンド  : IRを最適化するステージ ・バックエンド  : 最適化されたIRを機械語へ変換するステージ Goコンパイラ処理の概要 1. 字句解析 2. 構文解析 3. 型チェック 4. IRの構築 1. IRに対する最適化 2. IR(コンパイラAST)  の巡回 1. SSA形式の構築 2. 機械語生成 フロントエンド ミドルエンド バックエンド

Slide 25

Slide 25 text

25 Goコンパイラ処理の概要 フロントエンド ミドルエンド バックエンド //go:embed ディレクティブ (pragma)の解釈 //go:embed ディレクティブの チェック バイナリへの 埋め込み 1. 字句解析 2. 構文解析 3. 型チェック 4. IRの構築 1. IRに対する最適化 2. IR(コンパイラAST)  の巡回 1. SSA形式の構築 2. 機械語生成

Slide 26

Slide 26 text

26 フロントエンドでの処理 フロントエンド ミドルエンド バックエンド //go:embed ディレクティブ (pragma)の解釈 //go:embed ディレクティブの チェック バイナリへの 埋め込み 1. 字句解析 2. 構文解析 3. 型チェック 4. IRの構築 1. IRに対する最適化 2. IR(コンパイラAST)  の巡回 1. SSA形式の構築 2. 機械語生成

Slide 27

Slide 27 text

27 フロントエンドでの処理 フロントエンド ミドルエンド バックエンド //go:embed ディレクティブ (pragma)の解釈 //go:embed ディレクティブの チェック バイナリへの 埋め込み 1. 字句解析 2. 構文解析 3. 型チェック 4. IRの構築 1. IRに対する最適化 2. IR(コンパイラAST)  の巡回 1. SSA形式の構築 2. 機械語生成 🤔

Slide 28

Slide 28 text

28 言語仕様の一部ではないので、 Goの言語仕様 に書かれていない ・ALGOL 68の pragmats に由来し、Cで #pragma が採用されてから  この名称が浸透した ・機能として濫用することは望ましくないとコメントされている(ref) Goでは以下のようなコメントが Pragmaとして扱われる ・//go: で始まるコメント ・//line で始まるコメント これらのコメントを処理するハンドラを Pragma Handlerと呼ぶ Pragmaはコンパイラ /インタプリタ用のディレクティブ

Slide 29

Slide 29 text

29 ソースコードを Goの言語仕様 に沿ってパースする部分が主に関連する ・パース時に // で始まる文字列が来た場合  ・Pragma Handlerによって後続文字列をパースする   ・後続文字列に go:embed が来た場合    ・後続するパス部分をパース   ・パースされたembed用のpragmaを pragma.Embedsに格納 ・パース時に var による変数宣言が来た場合  ・var 宣言に、上記のpragmaをセットする //go:embed ディレクティブの解釈 日本語はわかりづらい......

Slide 30

Slide 30 text

30 処理をフロー図で考える ※ 関連部分のみを抜粋して要約されたもの

Slide 31

Slide 31 text

31 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる

Slide 32

Slide 32 text

32 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる

Slide 33

Slide 33 text

33 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる

Slide 34

Slide 34 text

34 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる

Slide 35

Slide 35 text

35 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる

Slide 36

Slide 36 text

36 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる

Slide 37

Slide 37 text

37 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる pragma.Embeds = pragmaEmbeds{ {pos, []string{"hello.txt"}}, }

Slide 38

Slide 38 text

38 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる pragma.Embeds == pragmaEmbeds{ {pos, []string{"hello.txt"}}, }

Slide 39

Slide 39 text

39 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる pragma.Embeds == pragmaEmbeds{ {pos, []string{"hello.txt"}}, }

Slide 40

Slide 40 text

40 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる pragma.Embeds == pragmaEmbeds{ {pos, []string{"hello.txt"}}, }

Slide 41

Slide 41 text

41 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる varDecl.Pragma = pragmaEmbeds{ {pos, []string{"hello.txt"}}, }

Slide 42

Slide 42 text

42 //go:embed hello.txt var hello string 具体例でパーサの気持ちになる varDecl.Pragma = pragmaEmbeds{ {pos, []string{"hello.txt"}}, } varDeclにpragma情報が セットされた👏

Slide 43

Slide 43 text

43 ソースコードで考える : Pragma Handler(ref)

Slide 44

Slide 44 text

44 ソースコードで考える : parseGoEmbedの実装(ref) パス指定のパターン パース前 (args string) パース後 ([]string, error) スペース区切り "a.txt b/c.go" []string{"a.txt", "b/c.go"}, nil ダブルクオート "\"hoge fuga.go\"" []string{"hoge fuga.go"}, nil バッククオート "`hoge fuga.go`" []string{"hoge fuga.go"}, nil

Slide 45

Slide 45 text

45 ソースコードで考える : VarDeclにPragmaをセット

Slide 46

Slide 46 text

46 ミドルエンドでの処理 1. 字句解析 2. 構文解析 3. 型チェック 4. IRの構築 1. SSA形式の構築 2. 機械語生成 フロントエンド ミドルエンド バックエンド //go:embed ディレクティブ (pragma)の解釈 //go:embed ディレクティブの チェック バイナリへの 埋め込み 1. IRに対する最適化 2. IR(コンパイラAST)  の巡回

Slide 47

Slide 47 text

47 ミドルエンドでの処理 1. 字句解析 2. 構文解析 3. 型チェック 4. IRの構築 1. SSA形式の構築 2. 機械語生成 フロントエンド ミドルエンド バックエンド //go:embed ディレクティブ (pragma)の解釈 //go:embed ディレクティブの チェック バイナリへの 埋め込み 1. IRに対する最適化 2. IR(コンパイラAST)  の巡回 🤔

Slide 48

Slide 48 text

48 AST(Abstract Syntax Tree): 抽象構文木 Results プログラムの文法構造を木 (tree)の形で表したもの ・例えば、 return 1, nil は Return Statements ReturnStmt Expr (1) Expr (nil)

Slide 49

Slide 49 text

49 go:embedディレクティブのチェック ・IR(コンパイラAST)の巡回時に以下の項目を確認する  ・"embed" パッケージをimportしていること  ・var a, b stringのように複数のnameを持つ変数宣言が対象ではないこと  ・変数が初期化されていないこと  ・関数内で利用されていないこと  ・Goのバージョンが1.16以上であること

Slide 50

Slide 50 text

50 ASTの巡回

Slide 51

Slide 51 text

51 nodeからVisitまで

Slide 52

Slide 52 text

52 VisitからcheckEmbedまで

Slide 53

Slide 53 text

53 checkEmbed

Slide 54

Slide 54 text

54 typecheck.Target へのセット ・チェック完了後にEmbed情報を ir.Embed としてセットする  ・これは後ほどバイナリにembedする際に利用される ・ ir.Embed は syntax.pragmaEmbed と殆ど同じ  ・IRへ変換する際に大きな変更は加えていない ir.Embed syntax.pragmaEmbed

Slide 55

Slide 55 text

55 typecheck.Target へのセット ・実際はセットの前にパッケージ情報のEncode処理が走る  ・本質的にはデータの書き換えはしていないので割愛

Slide 56

Slide 56 text

56 typecheck.Target へのセット

Slide 57

Slide 57 text

57 バックエンドでの処理 1. 字句解析 2. 構文解析 3. 型チェック 4. IRの構築 1. ASTに対する最適化 2. ASTの巡回 1. SSA形式の構築 2. 機械語生成 フロントエンド ミドルエンド バックエンド //go:embed ディレクティブ (pragma)の解釈 //go:embed ディレクティブの チェック バイナリへの 埋め込み

Slide 58

Slide 58 text

58 関連するバックエンドの処理は以下の2つ ・埋め込まれるファイル用のシンボルの準備  ・シンボルはプログラムにおける特定のオブジェクトやリソースを   一意に特定するためのもの  ・コンパイル/リンク関連の話になるので割愛 ・オブジェクトファイルへの書き込み  ・オブジェクトファイルは実行ファイルのバイナリに利用されるファイル  ・順序はオブジェクトファイル=>アーカイブ=>実行ファイル バックエンドにおける処理

Slide 59

Slide 59 text

59 シンボルの準備 : internal/staticdataパッケージ embed関連情報の書き込みをするパッケージ ・Go1.17でinternal/gcから分割された(ref) ざっくりとした処理 ・embed対象の型の確認  ・var変数の型に応じてembedString, embedBytes, embedFilesを返す ・オブジェクトファイルに書き込まれるシンボルをセット

Slide 60

Slide 60 text

60 staticdataパッケージ : 種類の確認 型がembed.FSなら embedFiles 型がstringなら embedString 型が[]uint8なら embedBytes ※ byteはuint8のalias(ref)

Slide 61

Slide 61 text

61 単一ファイルのシンボル設定 埋め込むファイルの シンボル情報を取得 データとして ファイルのシンボルをセット

Slide 62

Slide 62 text

62 ファイルを読み込む前に、ファイルサイズを見てから処理を分けている ・0B < size ≦ 1024B: 読み取ってstringシンボルとして保持 ・1024B < size < 2GB: 読み取らずにfileStringシンボルとして保持 ・size ≧ 2GB: シンボル埋め込みができない  ・対象のシンボルではサイズがint32で管理されているので(ref)  ・int32の最大値は2^31-1=2,147,483,647≧2*10^9 => 2GB 埋め込むファイルのシンボル

Slide 63

Slide 63 text

63 単一ファイルのシンボル設定 stringの場合はデータに 対するポインタとlenを持つ []byteの場合はcapも持つ

Slide 64

Slide 64 text

64 複数ファイルのシンボル設定 複数ファイルを保持するための準備

Slide 65

Slide 65 text

65 複数ファイルのシンボル設定 []byteとしてシンボルを保持

Slide 66

Slide 66 text

66 data blockへの書き込み シンボル情報を元にデータを書き込む ・ここでファイルを開いてデータを読み取って書き込む ファイルのopen ファイル読み出しと オブジェクトファイルへの 書き込み

Slide 67

Slide 67 text

67 embedパッケージ ・Goバイナリへ「埋め込まれた」ファイルに関するパッケージ ・2種類の埋め込み方法を提供  ・単一のファイルを埋め込む方法  ・複数のファイルをファイルシステムとして埋め込む方法 //go:embed ディレクティブで指定されたファイルが埋め込まれるまで ・cmd/compileパッケージのmain関数から読んでいけば良い ・フロントエンド: ディレクティブをパースしてpragmaとして保持 ・ミドルエンド: ディレクティブの条件チェック ・バックエンド: オブジェクトファイルへの埋め込み 3. まとめ 皆さんも興味のあるパッケージ実装を読もう!

Slide 68

Slide 68 text

68 ・関連Issues  ・静的ファイル埋め込み機能の提起: https://github.com/golang/go/issues/35950   ・Prototype: https://github.com/golang/go/issues/41191   ・io/fsパッケージ: https://github.com/golang/go/issues/41190  ・後続(抜粋)   ・import _ embedをimport embedで書けるように: https://github.com/golang/go/issues/62417   ・モジュールパスとファイル名にunicodeを使えるように: https://github.com/golang/go/issues/67562   ・map[string]でembedできるように: https://github.com/golang/go/issues/69595   ・-embed=compress flagが利用できるように: https://github.com/golang/go/issues/61322   ・親ディレクトリもembedできるように: https://github.com/golang/go/issues/58519   ・書き込み可能にできるように: https://github.com/golang/go/issues/45757   ・リモートリソースの埋め込み: https://github.com/golang/go/issues/56401 ・Draft Design  ・Design Doc: https://go.googlesource.com/proposal/+/master/design/draft-embed.md  ・Video: https://youtu.be/rmS-oWcBZaI  ・Implementation: https://go-review.googlesource.com/c/go/+/243945 ・反応  ・Hacker News: https://news.ycombinator.com/item?id=23933966 4. Appendix

Slide 69

Slide 69 text

69 ● Introduction to the Go compiler ● https://qiita.com/junjis0203/items/616c00086eb336153f4f#appendgroupメソッドと解析メソッド ● https://gist.github.com/junjis0203/d97c4fc5384ccaff231b60c2c9e6d2b4#file-montecarlo-file_sturcted-go ● https://github.com/golang/go/tree/master/src/cmd/compile ● https://zenn.dev/sasakiki/articles/f2ebdf66a524e4 ● https://qiita.com/propella/items/9d7b1dece283cf462cb9 ● https://cipepser.hatenablog.com/entry/go-tool-compile ● https://www.altoros.com/blog/golang-internals-part-3-the-linker-object-files-and-relocations/ ● https://zenn.dev/ngicks/articles/go-virtual-mesh-fs-for-os-copyfs ● https://zenn.dev/hsaki/articles/godoc-asm-ja ● https://github.com/golang/go/commit/4dfb5d91a86dfcc046ced03cee6e844df0751e41#diff-93e5af6a86a5745929c27e317b1 58e986257f9cf4b6904f6e01e13f9bbebaabeL11 ● https://qiita.com/junjis0203/items/616c00086eb336153f4f#cmdcompileinternalgc ● https://zenn.dev/rescuenow/articles/aeb7f2e8c110d0 ● https://qiita.com/sonatard/items/7b9b376f3420879a00d6 ● https://zenn.dev/hiroyukim/books/a21fbd6eb9fdda/viewer/981184 ● https://future-architect.github.io/articles/20210208/ ● https://zenn.dev/dqneo/articles/ce9459676a3303#embedタグがある場合のファイルの埋め込み 4. Appendix - 参考文献