[インデックス 1650] ファイルの概要
このコミットは、Go言語の標準ライブラリos
パッケージにReaddir
関数を追加するものです。Readdir
は、指定されたディレクトリ内のエントリ(ファイルやサブディレクトリ)の情報をDir
構造体の配列として返します。これにより、ディレクトリの内容をより詳細に、かつ効率的に取得できるようになります。特に、Readdirnames
関数がファイル名のみを返すのに対し、Readdir
は各エントリのメタデータ(ファイル名、サイズ、モードなど)を含むDir
構造体を返す点が重要です。
コミット
commit aba4c75408526256c7f44f131ed380d99c2f3632
Author: Rob Pike <r@golang.org>
Date: Mon Feb 9 11:24:35 2009 -0800
add Readdir: returns an array of Dir structures
R=rsc
DELTA=200 (176 added, 12 deleted, 12 changed)
OCL=24680
CL=24680
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aba4c75408526256c7f44f131ed380d99c2f3632
元コミット内容
add Readdir: returns an array of Dir structures
R=rsc
DELTA=200 (176 added, 12 deleted, 12 changed)
OCL=24680
CL=24680
変更の背景
このコミットが行われた2009年2月は、Go言語がまだ一般に公開される前の初期開発段階でした。当時のos
パッケージには、ディレクトリの内容をリストアップする機能としてReaddirnames
が存在しましたが、これはディレクトリ内のエントリの名前(文字列)のリストしか返しませんでした。
しかし、ファイルシステム操作においては、ファイル名だけでなく、そのファイルのタイプ(ディレクトリかファイルか)、サイズ、パーミッション、最終更新日時などのメタデータも同時に取得したいというニーズが頻繁に発生します。Readdirnames
だけでは、これらの情報を得るためには、取得した各ファイル名に対して別途Stat
システムコールを呼び出す必要があり、これはI/Oオーバーヘッドが大きく、非効率的でした。
このような背景から、ディレクトリ内のエントリのメタデータを一括して取得できるReaddir
関数の導入が決定されました。これにより、ディレクトリの内容をより効率的かつ包括的に扱うことが可能になり、ファイルシステム関連の操作を行うプログラムのパフォーマンスと利便性が向上しました。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
-
syscall.Dirent
構造体: Unix系システムにおいて、ディレクトリの内容を読み取るシステムコール(getdents
やgetdirentries
)が返すディレクトリエントリの情報を格納するためのC言語の構造体に対応するGo言語の表現です。この構造体には、通常、inode番号 (Ino
)、レコード長 (Reclen
)、ファイルタイプ、そしてファイル名 (Name
) などが含まれます。OSやアーキテクチャによってフィールドの配置やサイズが異なるため、Go言語ではsyscall
パッケージ内で各プラットフォームに特化した定義がされています。 -
Getdirentries
(Darwin/BSD系) とGetdents
(Linux系) システムコール: これらは、カーネルからディレクトリの内容を読み取るためのシステムコールです。Getdirentries
(macOS/Darwin): ファイルディスクリプタ、バッファ、バッファサイズ、そしてファイルオフセットを引数に取り、ディレクトリの内容をバッファに書き込みます。ファイルオフセットは、次に読み取るべき位置を示し、システムコールが成功すると更新されます。Getdents
(Linux): ファイルディスクリプタ、バッファ、バッファサイズを引数に取り、ディレクトリの内容をバッファに書き込みます。Linuxのgetdents
は、ファイルオフセットを内部的に管理するため、明示的にオフセットを渡す必要はありません。 これらのシステムコールは、ディレクトリ内のエントリをsyscall.Dirent
構造体の連続したバイト列としてバッファに格納します。
-
unsafe.Pointer
とuintptr
: Go言語のunsafe
パッケージは、型安全性をバイパスしてメモリを直接操作するための機能を提供します。unsafe.Pointer
: 任意の型のポインタを保持できる特殊なポインタ型です。C言語のvoid*
に似ています。uintptr
: ポインタの値を整数として表現する型です。ポインタ演算を行う際にunsafe.Pointer
と組み合わせて使用されます。 このコミットでは、syscall.Dirent
構造体の配列が格納されたバイトバッファから個々のDirent
構造体を取り出すために、unsafe.Pointer
とuintptr
を用いたポインタ演算が使用されています。これは、C言語のポインタ演算に相当する低レベルな操作であり、Goの型システムが提供する安全性を意図的に回避しています。
-
GoのファイルI/Oの歴史と
os
パッケージ: Go言語のos
パッケージは、オペレーティングシステムとのインタフェースを提供し、ファイルやディレクトリの操作、プロセス管理、環境変数へのアクセスなどを可能にします。Go言語の初期段階では、OS固有のシステムコールを直接呼び出すためのsyscall
パッケージがより頻繁に使用されていましたが、時間の経過とともに、より抽象化され、プラットフォーム非依存なos
パッケージのAPIが整備されていきました。このコミットは、その過渡期における重要な一歩と言えます。
技術的詳細
Readdir
関数の実装は、オペレーティングシステム(DarwinとLinux)によって異なるシステムコールを使用するため、src/lib/os/dir_amd64_darwin.go
とsrc/lib/os/dir_amd64_linux.go
の2つのファイルに分かれて実装されています。
Darwin (macOS) の実装 (dir_amd64_darwin.go
)
Darwin版のReaddir
は、syscall.Getdirentries
システムコールを使用します。
- ファイルオフセットの管理:
Getdirentries
はファイルオフセットを引数として取るため、syscall.Seek(fd.fd, 0, 1)
を呼び出して現在のファイルオフセットを取得し、それをbase
変数で管理します。これにより、ディレクトリの読み取り位置を正確に制御できます。 - バッファリング:
make([]byte, 8192)
で8KBのバイトスライスをバッファとして確保しています。これは、ディレクトリの内容を効率的に読み取るためのサイズです。コメントには「TODO(r): use fstatfs to find fs block size.」とあり、将来的にはファイルシステムのブロックサイズに合わせてバッファサイズを調整する意図があったことが伺えます。 syscall.Dirent
のパース:Getdirentries
がバッファに書き込んだ生バイト列から、unsafe.Pointer
とuintptr
を使って個々のsyscall.Dirent
構造体を抽出しています。dirent.Reclen
(レコード長)を使って、次のエントリの開始位置を計算し、バッファ内を順次走査します。Dir
構造体への変換とStat
呼び出し: 抽出したdirent
からファイル名を取得し、そのファイル名とディレクトリ名を結合して完全なパスを作成します。その後、Stat(dirname + filename)
を呼び出して、各エントリのメタデータ(ファイルタイプ、サイズ、パーミッションなど)を含むDir
構造体を取得しています。このStat
呼び出しは、ディレクトリ内のエントリごとに発生するため、I/Oオーバーヘッドの要因となる可能性があります。- 動的なスライス拡張:
dirs
スライスは、make([]Dir, 0, 100)
で初期容量100で作成されますが、エントリ数がそれを超える場合は、ndirs := make([]Dir, len(dirs), 2*len(dirs))
のように容量を倍にして新しいスライスを作成し、既存の要素をコピーすることで動的に拡張されます。
Linux の実装 (dir_amd64_linux.go
)
Linux版のReaddir
は、syscall.Getdents
システムコールを使用します。
- ファイルオフセットの管理: Linuxの
Getdents
はファイルオフセットを内部的に管理するため、Darwin版のように明示的にSeek
を呼び出す必要はありません。 - バッファリング:
make([]syscall.Dirent, 8192/unsafe.Sizeof(*new(syscall.Dirent)))
のように、syscall.Dirent
構造体のスライスをバッファとして確保しています。バッファサイズは、syscall.Dirent
のサイズに基づいて計算されます。 syscall.Dirent
のパース: Darwin版と同様に、unsafe.Pointer
とuintptr
を使ってバッファから個々のsyscall.Dirent
構造体を抽出します。dirent.Reclen
を使用して次のエントリへ進みます。Dir
構造体への変換とStat
呼び出し: Darwin版と同様に、ファイル名を取得し、Stat
を呼び出してDir
構造体を取得します。ここでも、各エントリに対するStat
呼び出しがオーバーヘッドとなる可能性が指摘されています。コメントには「TODO(r): Readdir duplicates a lot of Readdirnames. The other way would be to have Readdir (which could then be portable) call Readdirnames and then do the Stats. ... Also, it's possible given the nature of the Unix kernel that interleaving reads of the directory with stats (as done here) would work better than one big read of the directory followed by a long run of Stat calls.」とあり、実装の効率性に関する検討がなされていたことがわかります。- 動的なスライス拡張: Darwin版と同様に、
dirs
スライスは必要に応じて動的に拡張されます。
Readdirnames
の変更
Readdir
の追加に伴い、既存のReaddirnames
関数も一部修正されています。主に、dir
という変数名がdirent
に変更され、コードの可読性が向上しています。機能的な変更はありません。
テストの追加 (os_test.go
)
os_test.go
には、Readdir
関数の動作を検証するための新しいテストケースが追加されています。
dot
とetc
というグローバル変数が追加され、それぞれカレントディレクトリと/etc
ディレクトリに期待されるファイル名のリストが定義されています。testReaddirnames
関数が汎用化され、任意のディレクトリと期待される内容のリストを受け取れるようになりました。testReaddir
関数が新しく追加され、Readdir
の動作を検証します。この関数もtestReaddirnames
と同様に汎用的に設計されています。TestReaddirnames
とTestReaddir
というテスト関数が追加され、それぞれカレントディレクトリと/etc
ディレクトリに対してReaddirnames
とReaddir
を呼び出し、結果が期待通りであることを確認しています。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、以下の3つのファイルに集中しています。
-
src/lib/os/dir_amd64_darwin.go
:Readdir
関数の新規追加。- 既存の
Readdirnames
関数内の変数名dir
をdirent
に変更。
-
src/lib/os/dir_amd64_linux.go
:Readdir
関数の新規追加。- 既存の
Readdirnames
関数内の変数名dir
をdirent
に変更。
-
src/lib/os/os_test.go
:dot
とetc
というグローバルな文字列スライス変数の追加。testReaddirnames
関数の引数にdir
とcontents
を追加し、汎用化。testReaddir
関数の新規追加。TestReaddirnames
とTestReaddir
テスト関数の新規追加。
コアとなるコードの解説
src/lib/os/dir_amd64_darwin.go
および src/lib/os/dir_amd64_linux.go
の Readdir
関数
両OSの実装で共通する主要なロジックは以下の通りです。
// Negative count means read until EOF.
func Readdir(fd *FD, count int) (dirs []Dir, err *os.Error) {
dirname := fd.name
if dirname == "" {
dirname = "."
}
dirname += "/"
// ... (OS固有のバッファリングとシステムコール呼び出し) ...
for {
if count == 0 {
break
}
// ret, err2 := syscall.Getdirentries(...) (Darwin)
// ret, err2 := syscall.Getdents(...) (Linux)
// ...
if ret == 0 { // EOF
break
}
for w, i := uintptr(0), uintptr(0); i < uintptr(ret); i += w {
if count == 0 {
break
}
dirent := unsafe.Pointer((uintptr(unsafe.Pointer(&buf[0])) + i)).(*syscall.Dirent)
w = uintptr(dirent.Reclen)
if dirent.Ino == 0 { // Skip deleted or invalid entries
continue
}
count--
// 動的なスライス拡張
if len(dirs) == cap(dirs) {
ndirs := make([]Dir, len(dirs), 2*len(dirs))
for i := 0; i < len(dirs); i++ {
ndirs[i] = dirs[i]
}
dirs = ndirs
}
dirs = dirs[0 : len(dirs)+1]
filename := string(dirent.Name[0:dirent.Namlen]) // Darwin
// filename := string(dirent.Name[0:clen(dirent.Name)]) // Linux
// 各エントリに対してStatを呼び出し、詳細情報を取得
dirp, err := Stat(dirname + filename)
if dirp == nil || err != nil {
dirs[len(dirs)-1].Name = filename // Statに失敗した場合でもファイル名だけは設定
} else {
dirs[len(dirs)-1] = *dirp
}
}
}
return dirs, nil
}
dirname
の決定:fd.name
が空の場合はカレントディレクトリを表す"."
を使用し、末尾に"/"
を追加してパスを構築します。- ループ処理:
count
が0になるか、ディレクトリの終端(EOF)に達するまでループを続けます。count
が負の値の場合はEOFまで読み込みます。 syscall.Dirent
の抽出:unsafe.Pointer
とuintptr
を使ったポインタ演算により、システムコールが返したバイトバッファから個々のsyscall.Dirent
構造体を安全でない方法で取り出します。dirent.Reclen
は、次のディレクトリエントリまでのバイトオフセットを示します。- 無効なエントリのスキップ:
dirent.Ino == 0
は、通常、削除されたエントリや無効なエントリを示すため、これらはスキップされます。 dirs
スライスの動的拡張:dirs
スライスの容量が不足した場合、現在の2倍の容量を持つ新しいスライスを作成し、既存の要素をコピーして効率的に拡張します。Stat
呼び出し: 抽出したファイル名とディレクトリ名を結合して完全なパスを作成し、Stat
関数を呼び出してそのファイルのメタデータ(Dir
構造体)を取得します。Stat
が失敗した場合でも、ファイル名だけはDir
構造体に設定されます。
src/lib/os/os_test.go
のテストコード
var dot = []string{
"dir_amd64_darwin.go",
"dir_amd64_linux.go",
"os_env.go",
"os_error.go",
"os_file.go",
"os_test.go",
"os_time.go",
"os_types.go",
"stat_amd64_darwin.go",
"stat_amd64_linux.go",
}
var etc = []string{
"group",
"hosts",
"passwd",
}
func testReaddirnames(dir string, contents []string, t *testing.T) {
fd, err := Open(dir, O_RDONLY, 0)
defer fd.Close()
if err != nil {
t.Fatalf("open %q failed: %s\n", dir, err.String())
}
s, err2 := Readdirnames(fd, -1)
if err2 != nil {
t.Fatal("readdirnames . failed:", err)
}
for i, m := range contents {
found := false
for j, n := range s {
if m == n {
if found {
t.Error("present twice:", m)
}
found = true
}
}
if !found {
t.Error("could not find", m)
}
}
}
func testReaddir(dir string, contents []string, t *testing.T) {
fd, err := Open(dir, O_RDONLY, 0)
defer fd.Close()
if err != nil {
t.Fatalf("open %q failed: %s\n", dir, err.String())
}
s, err2 := Readdir(fd, -1)
if err2 != nil {
t.Fatal("readdir . failed:", err)
}
for i, m := range contents {
found := false
for j, n := range s {
if m == n.Name { // Dir構造体のNameフィールドと比較
if found {
t.Error("present twice:", m)
}
found = true
}
}
if !found {
t.Error("could not find", m)
}
}
}
func TestReaddirnames(t *testing.T) {
testReaddirnames(".", dot, t)
testReaddirnames("/etc", etc, t)
}
func TestReaddir(t *testing.T) {
testReaddir(".", dot, t)
testReaddir("/etc", etc, t)
}
dot
とetc
: テスト対象のディレクトリに期待されるファイル名のリストを定義しています。これにより、テストの期待値が明確になります。testReaddirnames
とtestReaddir
: これらのヘルパー関数は、指定されたディレクトリに対してReaddirnames
またはReaddir
を呼び出し、返されたファイル名(またはDir
構造体のName
フィールド)が期待されるリストに含まれているかを検証します。重複がないか、すべてのファイルが見つかったかを確認します。TestReaddirnames
とTestReaddir
: 実際のテストエントリポイントです。カレントディレクトリ(.
)と/etc
ディレクトリに対して、それぞれtestReaddirnames
とtestReaddir
を呼び出しています。これにより、異なるディレクトリに対する関数の動作が検証されます。
関連リンク
- Go言語の
os
パッケージのドキュメント: https://pkg.go.dev/os - Go言語の
syscall
パッケージのドキュメント: https://pkg.go.dev/syscall - Go言語の
unsafe
パッケージのドキュメント: https://pkg.go.dev/unsafe
参考にした情報源リンク
- Go言語のソースコード (golang/go GitHubリポジトリ): https://github.com/golang/go
- Unix系システムコールに関するドキュメント (例:
getdents
,getdirentries
manページ) - Go言語の初期開発に関する議論やメーリングリストのアーカイブ (Go言語の公開前の情報源は限られているため、一般的な知識とGoの進化の歴史から推測)
- Go言語の
os
パッケージの進化に関する記事やブログポスト (もしあれば) - Go言語の
unsafe
パッケージに関する解説記事I have generated the detailed technical explanation in Markdown format, including all the required sections. I have also incorporated information about thesyscall.Dirent
,Getdirentries
,Getdents
,unsafe.Pointer
,uintptr
, and the early history of Go's file I/O to provide comprehensive background and technical details.
I will now output the generated Markdown content to standard output.
# [インデックス 1650] ファイルの概要
このコミットは、Go言語の標準ライブラリ`os`パッケージに`Readdir`関数を追加するものです。`Readdir`は、指定されたディレクトリ内のエントリ(ファイルやサブディレクトリ)の情報を`Dir`構造体の配列として返します。これにより、ディレクトリの内容をより詳細に、かつ効率的に取得できるようになります。特に、`Readdirnames`関数がファイル名のみを返すのに対し、`Readdir`は各エントリのメタデータ(ファイル名、サイズ、モードなど)を含む`Dir`構造体を返す点が重要です。
## コミット
commit aba4c75408526256c7f44f131ed380d99c2f3632 Author: Rob Pike r@golang.org Date: Mon Feb 9 11:24:35 2009 -0800
add Readdir: returns an array of Dir structures
R=rsc
DELTA=200 (176 added, 12 deleted, 12 changed)
OCL=24680
CL=24680
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/aba4c75408526256c7f44f131ed380d99c2f3632](https://github.com/golang/go/commit/aba4c75408526256c7f44f131ed380d99c2f3632)
## 元コミット内容
add Readdir: returns an array of Dir structures
R=rsc DELTA=200 (176 added, 12 deleted, 12 changed) OCL=24680 CL=24680
## 変更の背景
このコミットが行われた2009年2月は、Go言語がまだ一般に公開される前の初期開発段階でした。当時の`os`パッケージには、ディレクトリの内容をリストアップする機能として`Readdirnames`が存在しましたが、これはディレクトリ内のエントリの名前(文字列)のリストしか返しませんでした。
しかし、ファイルシステム操作においては、ファイル名だけでなく、そのファイルのタイプ(ディレクトリかファイルか)、サイズ、パーミッション、最終更新日時などのメタデータも同時に取得したいというニーズが頻繁に発生します。`Readdirnames`だけでは、これらの情報を得るためには、取得した各ファイル名に対して別途`Stat`システムコールを呼び出す必要があり、これはI/Oオーバーヘッドが大きく、非効率的でした。
このような背景から、ディレクトリ内のエントリのメタデータを一括して取得できる`Readdir`関数の導入が決定されました。これにより、ディレクトリの内容をより効率的かつ包括的に扱うことが可能になり、ファイルシステム関連の操作を行うプログラムのパフォーマンスと利便性が向上しました。
## 前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
* **`syscall.Dirent`構造体**:
Unix系システムにおいて、ディレクトリの内容を読み取るシステムコール(`getdents`や`getdirentries`)が返すディレクトリエントリの情報を格納するためのC言語の構造体に対応するGo言語の表現です。この構造体には、通常、inode番号 (`Ino`)、レコード長 (`Reclen`)、ファイルタイプ、そしてファイル名 (`Name`) などが含まれます。OSやアーキテクチャによってフィールドの配置やサイズが異なるため、Go言語では`syscall`パッケージ内で各プラットフォームに特化した定義がされています。
* **`Getdirentries` (Darwin/BSD系) と `Getdents` (Linux系) システムコール**:
これらは、カーネルからディレクトリの内容を読み取るためのシステムコールです。
* `Getdirentries` (macOS/Darwin): ファイルディスクリプタ、バッファ、バッファサイズ、そしてファイルオフセットを引数に取り、ディレクトリの内容をバッファに書き込みます。ファイルオフセットは、次に読み取るべき位置を示し、システムコールが成功すると更新されます。
* `Getdents` (Linux): ファイルディスクリプタ、バッファ、バッファサイズを引数に取り、ディレクトリの内容をバッファに書き込みます。Linuxの`getdents`は、ファイルオフセットを内部的に管理するため、明示的にオフセットを渡す必要はありません。
これらのシステムコールは、ディレクトリ内のエントリを`syscall.Dirent`構造体の連続したバイト列としてバッファに格納します。
* **`unsafe.Pointer`と`uintptr`**:
Go言語の`unsafe`パッケージは、型安全性をバイパスしてメモリを直接操作するための機能を提供します。
* `unsafe.Pointer`: 任意の型のポインタを保持できる特殊なポインタ型です。C言語の`void*`に似ています。
* `uintptr`: ポインタの値を整数として表現する型です。ポインタ演算を行う際に`unsafe.Pointer`と組み合わせて使用されます。
このコミットでは、`syscall.Dirent`構造体の配列が格納されたバイトバッファから個々の`Dirent`構造体を取り出すために、`unsafe.Pointer`と`uintptr`を用いたポインタ演算が使用されています。これは、C言語のポインタ演算に相当する低レベルな操作であり、Goの型システムが提供する安全性を意図的に回避しています。
* **GoのファイルI/Oの歴史と`os`パッケージ**:
Go言語の`os`パッケージは、オペレーティングシステムとのインタフェースを提供し、ファイルやディレクトリの操作、プロセス管理、環境変数へのアクセスなどを可能にします。Go言語の初期段階では、OS固有のシステムコールを直接呼び出すための`syscall`パッケージがより頻繁に使用されていましたが、時間の経過とともに、より抽象化され、プラットフォーム非依存な`os`パッケージのAPIが整備されていきました。このコミットは、その過渡期における重要な一歩と言えます。
## 技術的詳細
`Readdir`関数の実装は、オペレーティングシステム(DarwinとLinux)によって異なるシステムコールを使用するため、`src/lib/os/dir_amd64_darwin.go`と`src/lib/os/dir_amd64_linux.go`の2つのファイルに分かれて実装されています。
### Darwin (macOS) の実装 (`dir_amd64_darwin.go`)
Darwin版の`Readdir`は、`syscall.Getdirentries`システムコールを使用します。
1. **ファイルオフセットの管理**: `Getdirentries`はファイルオフセットを引数として取るため、`syscall.Seek(fd.fd, 0, 1)`を呼び出して現在のファイルオフセットを取得し、それを`base`変数で管理します。これにより、ディレクトリの読み取り位置を正確に制御できます。
2. **バッファリング**: `make([]byte, 8192)`で8KBのバイトスライスをバッファとして確保しています。これは、ディレクトリの内容を効率的に読み取るためのサイズです。コメントには「TODO(r): use fstatfs to find fs block size.」とあり、将来的にはファイルシステムのブロックサイズに合わせてバッファサイズを調整する意図があったことが伺えます。
3. **`syscall.Dirent`のパース**: `Getdirentries`がバッファに書き込んだ生バイト列から、`unsafe.Pointer`と`uintptr`を使って個々の`syscall.Dirent`構造体を抽出しています。`dirent.Reclen`(レコード長)を使って、次のエントリの開始位置を計算し、バッファ内を順次走査します。
4. **`Dir`構造体への変換と`Stat`呼び出し**: 抽出した`dirent`からファイル名を取得し、そのファイル名とディレクトリ名を結合して完全なパスを作成します。その後、`Stat(dirname + filename)`を呼び出して、各エントリのメタデータ(ファイルタイプ、サイズ、パーミッションなど)を含む`Dir`構造体を取得しています。この`Stat`呼び出しは、ディレクトリ内のエントリごとに発生するため、I/Oオーバーヘッドの要因となる可能性があります。
5. **動的なスライス拡張**: `dirs`スライスは、`make([]Dir, 0, 100)`で初期容量100で作成されますが、エントリ数がそれを超える場合は、`ndirs := make([]Dir, len(dirs), 2*len(dirs))`のように容量を倍にして新しいスライスを作成し、既存の要素をコピーすることで動的に拡張されます。
### Linux の実装 (`dir_amd64_linux.go`)
Linux版の`Readdir`は、`syscall.Getdents`システムコールを使用します。
1. **ファイルオフセットの管理**: Linuxの`Getdents`はファイルオフセットを内部的に管理するため、Darwin版のように明示的に`Seek`を呼び出す必要はありません。
2. **バッファリング**: `make([]syscall.Dirent, 8192/unsafe.Sizeof(*new(syscall.Dirent)))`のように、`syscall.Dirent`構造体のスライスをバッファとして確保しています。バッファサイズは、`syscall.Dirent`のサイズに基づいて計算されます。
3. **`syscall.Dirent`のパース**: Darwin版と同様に、`unsafe.Pointer`と`uintptr`を使ってバッファから個々の`syscall.Dirent`構造体を抽出します。`dirent.Reclen`を使用して次のエントリへ進みます。
4. **`Dir`構造体への変換と`Stat`呼び出し**: Darwin版と同様に、ファイル名を取得し、`Stat`を呼び出して`Dir`構造体を取得します。ここでも、各エントリに対する`Stat`呼び出しがオーバーヘッドとなる可能性が指摘されています。コメントには「TODO(r): Readdir duplicates a lot of Readdirnames. The other way would be to have Readdir (which could then be portable) call Readdirnames and then do the Stats. ... Also, it's possible given the nature of the Unix kernel that interleaving reads of the directory with stats (as done here) would work better than one big read of the directory followed by a long run of Stat calls.」とあり、実装の効率性に関する検討がなされていたことがわかります。
5. **動的なスライス拡張**: Darwin版と同様に、`dirs`スライスは必要に応じて動的に拡張されます。
### `Readdirnames`の変更
`Readdir`の追加に伴い、既存の`Readdirnames`関数も一部修正されています。主に、`dir`という変数名が`dirent`に変更され、コードの可読性が向上しています。機能的な変更はありません。
### テストの追加 (`os_test.go`)
`os_test.go`には、`Readdir`関数の動作を検証するための新しいテストケースが追加されています。
* `dot`と`etc`というグローバル変数が追加され、それぞれカレントディレクトリと`/etc`ディレクトリに期待されるファイル名のリストが定義されています。
* `testReaddirnames`関数が汎用化され、任意のディレクトリと期待される内容のリストを受け取れるようになりました。
* `testReaddir`関数が新しく追加され、`Readdir`の動作を検証します。この関数も`testReaddirnames`と同様に汎用的に設計されています。
* `TestReaddirnames`と`TestReaddir`というテスト関数が追加され、それぞれカレントディレクトリと`/etc`ディレクトリに対して`Readdirnames`と`Readdir`を呼び出し、結果が期待通りであることを確認しています。
## コアとなるコードの変更箇所
このコミットのコアとなる変更は、以下の3つのファイルに集中しています。
1. **`src/lib/os/dir_amd64_darwin.go`**:
* `Readdir`関数の新規追加。
* 既存の`Readdirnames`関数内の変数名`dir`を`dirent`に変更。
2. **`src/lib/os/dir_amd64_linux.go`**:
* `Readdir`関数の新規追加。
* 既存の`Readdirnames`関数内の変数名`dir`を`dirent`に変更。
3. **`src/lib/os/os_test.go`**:
* `dot`と`etc`というグローバルな文字列スライス変数の追加。
* `testReaddirnames`関数の引数に`dir`と`contents`を追加し、汎用化。
* `testReaddir`関数の新規追加。
* `TestReaddirnames`と`TestReaddir`テスト関数の新規追加。
## コアとなるコードの解説
### `src/lib/os/dir_amd64_darwin.go` および `src/lib/os/dir_amd64_linux.go` の `Readdir` 関数
両OSの実装で共通する主要なロジックは以下の通りです。
```go
// Negative count means read until EOF.
func Readdir(fd *FD, count int) (dirs []Dir, err *os.Error) {
dirname := fd.name
if dirname == "" {
dirname = "."
}
dirname += "/"
// ... (OS固有のバッファリングとシステムコール呼び出し) ...
for {
if count == 0 {
break
}
// ret, err2 := syscall.Getdirentries(...) (Darwin)
// ret, err2 := syscall.Getdents(...) (Linux)
// ...
if ret == 0 { // EOF
break
}
for w, i := uintptr(0), uintptr(0); i < uintptr(ret); i += w {
if count == 0 {
break
}
dirent := unsafe.Pointer((uintptr(unsafe.Pointer(&buf[0])) + i)).(*syscall.Dirent)
w = uintptr(dirent.Reclen)
if dirent.Ino == 0 { // Skip deleted or invalid entries
continue
}
count--
// 動的なスライス拡張
if len(dirs) == cap(dirs) {
ndirs := make([]Dir, len(dirs), 2*len(dirs))
for i := 0; i < len(dirs); i++ {
ndirs[i] = dirs[i]
}
dirs = ndirs
}
dirs = dirs[0 : len(dirs)+1]
filename := string(dirent.Name[0:dirent.Namlen]) // Darwin
// filename := string(dirent.Name[0:clen(dirent.Name)]) // Linux
// 各エントリに対してStatを呼び出し、詳細情報を取得
dirp, err := Stat(dirname + filename)
if dirp == nil || err != nil {
dirs[len(dirs)-1].Name = filename // Statに失敗した場合でもファイル名だけは設定
} else {
dirs[len(dirs)-1] = *dirp
}
}
}
return dirs, nil
}
dirname
の決定:fd.name
が空の場合はカレントディレクトリを表す"."
を使用し、末尾に"/"
を追加してパスを構築します。- ループ処理:
count
が0になるか、ディレクトリの終端(EOF)に達するまでループを続けます。count
が負の値の場合はEOFまで読み込みます。 syscall.Dirent
の抽出:unsafe.Pointer
とuintptr
を使ったポインタ演算により、システムコールが返したバイトバッファから個々のsyscall.Dirent
構造体を安全でない方法で取り出します。dirent.Reclen
は、次のディレクトリエントリまでのバイトオフセットを示します。- 無効なエントリのスキップ:
dirent.Ino == 0
は、通常、削除されたエントリや無効なエントリを示すため、これらはスキップされます。 dirs
スライスの動的拡張:dirs
スライスの容量が不足した場合、現在の2倍の容量を持つ新しいスライスを作成し、既存の要素をコピーして効率的に拡張します。Stat
呼び出し: 抽出したファイル名とディレクトリ名を結合して完全なパスを作成し、Stat
関数を呼び出してそのファイルのメタデータ(Dir
構造体)を取得します。Stat
が失敗した場合でも、ファイル名だけはDir
構造体に設定されます。
src/lib/os/os_test.go
のテストコード
var dot = []string{
"dir_amd64_darwin.go",
"dir_amd64_linux.go",
"os_env.go",
"os_error.go",
"os_file.go",
"os_test.go",
"os_time.go",
"os_types.go",
"stat_amd64_darwin.go",
"stat_amd64_linux.go",
}
var etc = []string{
"group",
"hosts",
"passwd",
}
func testReaddirnames(dir string, contents []string, t *testing.T) {
fd, err := Open(dir, O_RDONLY, 0)
defer fd.Close()
if err != nil {
t.Fatalf("open %q failed: %s\n", dir, err.String())
}
s, err2 := Readdirnames(fd, -1)
if err2 != nil {
t.Fatal("readdirnames . failed:", err)
}
for i, m := range contents {
found := false
for j, n := range s {
if m == n {
if found {
t.Error("present twice:", m)
}
found = true
}
}
if !found {
t.Error("could not find", m)
}
}
}
func testReaddir(dir string, contents []string, t *testing.T) {
fd, err := Open(dir, O_RDONLY, 0)
defer fd.Close()
if err != nil {
t.Fatalf("open %q failed: %s\n", dir, err.String())
}
s, err2 := Readdir(fd, -1)
if err2 != nil {
t.Fatal("readdir . failed:", err)
}
for i, m := range contents {
found := false
for j, n := range s {
if m == n.Name { // Dir構造体のNameフィールドと比較
if found {
t.Error("present twice:", m)
}
found = true
}
}
if !found {
t.Error("could not find", m)
}
}
}
func TestReaddirnames(t *testing.T) {
testReaddirnames(".", dot, t)
testReaddirnames("/etc", etc, t)
}
func TestReaddir(t *testing.T) {
testReaddir(".", dot, t)
testReaddir("/etc", etc, t)
}
dot
とetc
: テスト対象のディレクトリに期待されるファイル名のリストを定義しています。これにより、テストの期待値が明確になります。testReaddirnames
とtestReaddir
: これらのヘルパー関数は、指定されたディレクトリに対してReaddirnames
またはReaddir
を呼び出し、返されたファイル名(またはDir
構造体のName
フィールド)が期待されるリストに含まれているかを検証します。重複がないか、すべてのファイルが見つかったかを確認します。TestReaddirnames
とTestReaddir
: 実際のテストエントリポイントです。カレントディレクトリ(.
)と/etc
ディレクトリに対して、それぞれtestReaddirnames
とtestReaddir
を呼び出しています。これにより、異なるディレクトリに対する関数の動作が検証されます。
関連リンク
- Go言語の
os
パッケージのドキュメント: https://pkg.go.dev/os - Go言語の
syscall
パッケージのドキュメント: https://pkg.go.dev/syscall - Go言語の
unsafe
パッケージのドキュメント: https://pkg.go.dev/unsafe
参考にした情報源リンク
- Go言語のソースコード (golang/go GitHubリポジトリ): https://github.com/golang/go
- Unix系システムコールに関するドキュメント (例:
getdents
,getdirentries
manページ) - Go言語の初期開発に関する議論やメーリングリストのアーカイブ (Go言語の公開前の情報源は限られているため、一般的な知識とGoの進化の歴史から推測)
- Go言語の
os
パッケージの進化に関する記事やブログポスト (もしあれば) - Go言語の
unsafe
パッケージに関する解説記事