Go에는 입출력 추상화 인터페이스인 io.Reader io.Writer 가 존재합니다.

io/fs 는 파일 시스탬 추상화 인터페이스를 제공합니다.

os 패키지에서 io/fs 구현

// io/fs package

type FS interface {
	Open(name string) (File, error)
}
// io/fs package

type File interface {
	Stat() (FileInfo, error)
	Read([]byte) (int, error)
	Close() error
}
// os package

func DirFS(dir string) fs.FS {
	return dirFS(dir)
}

func containsAny(s, chars string) bool {
	for i := 0; i < len(s); i++ {
		for j := 0; j < len(chars); j++ {
			if s[i] == chars[j] {
				return true
			}
		}
	}
	return false
}

type dirFS string

func (dir dirFS) Open(name string) (fs.File, error) {
	if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
		return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
	}
	f, err := Open(string(dir) + "/" + name)
	if err != nil {
		return nil, err // nil fs.File
	}
	return f, nil
}

os 패키지의 DirFS 함수는 디렉토리의 경로를 받아와서 dirFS 로 형변환 합니다. dirFS 는 결국 내부적으로는 string 타입입니다.

fs.FS 인터페이스를 구현하기 위해 os 패키지에서 Open 함수를 정의 했습니다. 이 함수는 FS 의 값을 string 으로 형 변환 해서, 인자로 받은 파일 이름을 붙여 파일을 읽기모드 로 오픈 후 fs.File 인터페이스로 리턴 합니다.

요는 os 패키지에서 io/fsfs.FS 는 디렉토리로, 사용 되고, fs.FS 인터페이스 구현 함수인 Open 은 디렉토리의 하위 파일을 오픈하는 함수 입니다.

예) 파일 읽기

package main

import (
	"fmt"
	"io/fs"
	"io/ioutil"
	"os"
)

func main() {
	// main파일의 위치를 FS로 얻어온다.
	wd := os.DirFS("./")

	// #1
    // wd 디렉토리에 "test.txt"를 오픈 한다. 이는 fs.File 인터체이스를 리턴 한다.
	modFile, err := wd.Open("test.txt")

	if err != nil {
		panic(err)
	}

	// fs.File은 io.Reader를 포함 하므로,
    // io.Reader를 받는 ioutil.Reader로 파일을 읽는다.
	data, err := ioutil.ReadAll(modFile)

	if err != nil {
		panic(err)
	}

	fmt.Printf("%s", data)

	// #2
    // 혹은, io/fs의 제공함수인 ReadFile 함수로 파일을 읽는다.
	data, err = fs.ReadFile(wd, "test.txt")

	if err != nil {
		panic(err)
	}

	fmt.Printf("%s", data)
}

io/fs 의 함수

func Glob(fsys FS, pattern string) (matches []string, err error)

패턴과 일치하는 모든 파일의 이름을 반환하거나 일치하는 파일이 없으면 nil을 반환합니다.

func main() {
	wd := os.DirFS("./")

	mats, err := fs.Glob(wd, "go.*")

	if err != nil {
		panic(err)
	}

	fmt.Printf("%#v\n", mats)
}
[]string{"go.mod", "go.sum"}
디랙토리 내 파일

func ReadFile(fsys FS, name string) ([]byte, error)

fsys 하위 파일 name 을 EOF 까지 읽습니다. 내부적으로 Open Read Close 함수가 사용 됩니다.

func main() {
	wd := os.DirFS("./")

	data, err := fs.ReadFile(wd, "main.go")

	if err != nil {
		panic(err)
	}

	fmt.Printf("%s\n", data)
}
 .
 .
 .
func main() {
	wd := os.DirFS("./")

	data, err := fs.ReadFile(wd, "main.go")

	if err != nil {
		panic(err)
	}

	fmt.Printf("%s\n", data)
}

func ValidPath (name string ) bool

입력 받은 경로가 Open 가능한, 유효한 경로인지 검증 합니다. . .. 은 사용 불가 합니다. 루트가 지정되지 않은 슬래시로 구분 된 경로로 입력해야 합니다.

func main() {
	p1 := fs.ValidPath("boo/main.go")
	p2 := fs.ValidPath("foo/main.go")
	fmt.Printf("%t\n%t\n", p1, p2)
}
true
true

func WalkDir(fsys FS, root string, fn WalkDirFunc) error

fsys 에서 root 디렉토리 부터 파일 트리를 탐색 합니다. 발견된 엔트리는 fn 함수로 전달 됩니다.

func main() {
	wd := os.DirFS("./")

	err := fs.WalkDir(wd, ".", func(path string, d fs.DirEntry, err error) error {
		// 탐색한 엔트리 오픈 에러 처리
		if err != nil {
			println(err)
			return nil
		}

		// 엔트리 경로, 이름 출력
		fmt.Printf("%s - %s\n", path, d.Name())

		// 만약 파일 이름이 ee 일 경우
		if d.Name() == "ee" {
			// 에러 리턴, WalkDir 함수 즉시 종료 후 에러 리턴
			return errors.New("found ee")
		}

		return nil
	})

	if err != nil {
		panic(err)
	}
}
. - .
dd - dd
dd/ee - ee
panic: found ee

goroutine 1 [running]:
main.main()
        /home/dev/project/boo/main.go:28 +0xa8
exit status 2
탐색한 디랙토리의 구조

DirEntry 인터페이스

type DirEntry interface {
    Name() string
    IsDir() bool
    Type() FileMode
    Info() (FileInfo, error)
}

func ReadDir(fsys FS, name string) ([]DirEntry, error)

fsys 의 하위 디렉토리 name 디렉토리를 읽습니다.

func main() {

	wd := os.DirFS("./")

	dent, err := fs.ReadDir(wd, "dd")

	if err != nil {
		panic(err)
	}

	for _, n := range dent {
		println(n.Name())
	}
}

func Sub(fsys FS, dir string) (FS, error)

fsys 의 하위 디렉토리 name 을 받아 io/fs 로 오픈 하여 리턴 합니다.