yikegaya’s blog

yikegayaのブログ

「Goならわかるシステムプログラミング」の9章を読んだ(ファイルシステムの基礎と Go言語の標準パッケージ)

「Goならわかるシステムプログラミング」の9章(ファイルシステムの基礎と Go言語の標準パッケージ)を写経しつつ読んだのでメモ。

ファイルシステムとは

コンピューターにはさまざまなストレージが接続されています。ハードディスクやSSD、取り外し可能なSDカード、読み込み専用のDVD-ROMやBlu-Ray、書き込み可能なDVD-RWなど、種類を網羅するのが困難なほどです。 種類はいろいろありますが、どのストレージも、基本的にはビットの羅列を保存できるだけです。そこで、そのストレージスペースを、特定の決まったルールで管理するための仕組みが必要になります。 たとえば、自分のローカルフォルダにあるテキストファイルをエディタで開き、編集して書き込みたいとします。ストレージのどこかにテキストファイルの内容を表すビット列があるはずですが、その実体のある場所を、ファイル名から探し出せる必要があります。 また、そこから内容を読み込んだり、新しい内容を上書きすることが、アプリケーションから不自由なく実現できなければなりません。そのためにOSに備わっている仕組みがファイルシステムです。

ファイル操作コード例

ファイル作成、読み込み

package main

import (
    "fmt"
    "io"
    "os"
)

func open() {
    file, err := os.Create("textfile.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    io.WriteString(file, "New file content\n")
}

func read() {
    file, err := os.Open("textfile.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    fmt.Println("Read file:")
    io.Copy(os.Stdout, file)
}

func main() {
    open()
    read()
}

追記モード

func append() {
    file, err := os.OpenFile("textfile.txt", os.O_RDWR|os.O_APPEND, 0666)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    io.WriteString(file, "Appended content\n")
}
// リネーム
os.Rename("old_name.txt", "new_name.txt")
// 移動
os.Rename("olddir/file.txt", "newdir/file.txt")

バイスやドライブが異なる場合にはファイルを開いてコピーする必要あり

oldFile, err := os.Open("old_name.txt")
if err != nil {
    panic(err)
}
newFile, err := os.Create("/other_device/new_file.txt")
if err != nil {
    panic(err)
}
defer newFile.Close()
_, err = io.Copy(newFile, oldFile)
if err != nil {
    panic(err)
}
oldFile.Close()
    os.Remove("old_name.txt")
}

ディレクトリの操作

// フォルダを 1 階層だけ作成
os.Mkdir("setting", 0755)
// 深いフォルダを 1 回で作成
os.MkdirAll("setting/myapp/networksettings", 0755)

ファイル、ディレクトリの削除

// ファイルや空のディレクトリの削除
os.Remove("server.log")
// ディレクトリを中身ごと削除
os.RemoveAll("workdir")

特定の長さで切り落とす

// 先頭 100 バイトで切る
os.Truncate("server.log", 100)
// Truncate メソッドを利用する場合
file, _ := os.Open("server.log")
file.Truncate(100)

リネーム、移動

// リネーム
os.Rename("old_name.txt", "new_name.txt")
// 移動
os.Rename("olddir/file.txt", "newdir/file.txt")
// 移動先はディレクトリではダメ
os.Rename("olddir/file.txt", "newdir/") // エラー発生 !

ファイルの属性の取得

os.Stat()と、os.LStat()で取得できる。 対象がシンボリックリンクだった場合、os.Stat()はリンク先の情報、os.LStat()はそのシンボリックリンクの情報を取得する。

サンプルコード

package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) == 1 {
        fmt.Printf("%s [exec file name]", os.Args[0])
        os.Exit(1)
    }
    info, err := os.Stat(os.Args[1])
    if err == os.ErrNotExist {
        fmt.Printf("file not found: %s\n", os.Args[1])
    } else if err != nil {
        panic(err)
    }
    fmt.Println("Fileinfo")
    fmt.Printf(" ファイル名: %v\n", info.Name())
    fmt.Printf(" サイズ: %v\n", info.Size())
    fmt.Printf(" 変更日時 %v\n", info.ModTime())
    fmt.Println("Mode()")
    fmt.Printf(" ディレクトリ? %v\n", info.Mode().IsDir())
    fmt.Printf(" 読み書き可能な通常ファイル? %v\n", info.Mode().IsRegular())
    fmt.Printf(" Unix のファイルアクセス権限ビット %o\n", info.Mode().Perm())
    fmt.Printf(" モードのテキスト表現 %v\n", info.Mode().String())
}

属性の変更

// ファイルのモードを変更
os.Chmod("setting.txt", 0644)
// ファイルのオーナーを変更
os.Chown("setting.txt", os.Getuid(), os.Getgid())
// ファイルの最終アクセス日時と変更日時を変更
os.Chtimes("setting.txt", time.Now(), time.Now())

ハードリンク、シンボリックリンク

// ハードリンク
os.Link("oldfile.txt", "newfile.txt")
// シンボリックリンク
os.Symlink("oldfile.txt", "newfile-symlink.txt")
// シンボリックリンクのリンク先を取得
link, err := os.ReadLink("newfile-symlink.txt")

ディレクトリ情報の取得

package main

import (
    "fmt"
    "os"
)

func main() {
    dir, err := os.Open("/")
    if err != nil {
        panic(err)
    }
    fileInfos, err := dir.Readdir(-1)
    if err != nil {
        panic(err)
    }
    for _, fileInfo := range fileInfos {
        if fileInfo.IsDir() {
            fmt.Printf("[Dir] %s\n", fileInfo.Name())
        } else {
            fmt.Printf("[File] %s\n", fileInfo.Name())
        }
    }
}

path/filepathパッケージ

ディレクトリのパスとファイル名を連結

package main

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

func main() {
    fmt.Printf("Temp file     Path: %s\n", filepath.Join(os.TempDir(), "temp.txt"))
}

パスを分割

func main() {
    dir, name := filepath.Split(os.Getenv("GOPATH"))
    fmt.Printf("Dir: %s, Name: %s\n", dir, name)
}

filepath.SplitList()

環境変数の値などにある「複数のパスを1つのテキストにまとめたもの」を分解するのに使う。

filepath.SplitList()を使ってwhichコマンドを再実装した例

package main

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

func main() {
    if len(os.Args) == 1 {
        fmt.Printf("%s [exec file name]", os.Args[0])
        os.Exit(1)
    }
    for _, path := range filepath.SplitList(os.Getenv("PATH")) {
        execpath := filepath.Join(path, os.Args[1])
        _, err := os.Stat(execpath)
        if !os.IsNotExist(err) {
            fmt.Println(execpath)
            return
        }
    }
    os.Exit(1)
}
go run main.go ls
/bin/ls

パスのクリーン化

パス表記の文字列をきれいに整えたいことがあります。filepath.Clean()関数を使うと、重複したセパレータを除去したり、上に行ったり下に降りたりを考慮して/abc/../def/からabc/..の部分を削除したり、現在のパス「.」を削除したりすることが可能です。 絶対パスに変換する filepath.Abs() や、基準のパスから相対パスを算出するfilepath.Rel()といった関数も、パス表記の整形に使えます。

コード例

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    fmt.Println(filepath.Clean("./path/filepath/../path.go"))

    abspath, _ := filepath.Abs("path/filepath/path_unix.go")
    fmt.Println(abspath)

    relpath, _ := filepath.Rel("/usr/local/go/src",
                               "/usr/local/go/src/path/filepath/path.go")
    fmt.Println(relpath)
}

環境変数

環境変数については、osパッケージのExpandEnv()を使って展開できる。

path := os.ExpandEnv("${GOPATH}/src/github.com/shibukawa/tobubus")
fmt.Println(path)
// /Users/shibu/gopath/src/github.com/shibukawa/tobubus

ディレクトリのトラバース

ディレクトリのような木構造をすべてたどることを、コンピューター用語ではトラバースといいます。filepathパッケージには、ディレクトリのトラバースに便利なfilepath.Walk()という関数もあります。この関数は、ディレクトリの木構造深さ優先探索でたどります。

指定したディレクトリ以下を探索して画像ファイルのファイル名を集めてくるコード

package main

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

var imageSuffix = map[string]bool{
    ".jpg": true,
    ".jpeg": true,
    ".png": true,
    ".webp": true,
    ".gif": true,
    ".tiff": true,
    ".eps": true,
}

func main() {
    if len(os.Args) == 1 {
        fmt.Printf(`Find images
      Usage:
      %s [path to find]
      `, os.Args[0])
        return
    }
    root := os.Args[1]
    err := filepath.Walk(root,
        func(path string, info os.FileInfo, err error) error {
            if info.IsDir() {
                if info.Name() == "_build" {
                    return filepath.SkipDir
                }
            return nil
        }
        ext := strings.ToLower(filepath.Ext(info.Name()))
        if imageSuffix[ext] {
            rel, err := filepath.Rel(root, path)
            if err != nil {
                return nil
            }
            fmt.Printf("%s\n", rel)
        }
        return nil
    })
    if err != nil {
        fmt.Println(1, err)
    }
}