Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

29장. 파일 입출력

파일을 읽고 쓰는 일은 거의 모든 프로그램의 기본 작업이다. 설정을 읽고, 로그를 남기고, 결과를 저장하고.

Go 는 파일 입출력을 위한 표준 도구가 잘 갖춰져 있다. 이 장에서 자주 쓰는 패턴을 익힌다.

목표:

  • 파일을 열고 안전하게 닫기
  • 전체 / 줄 단위 / 청크 단위 읽기
  • 전체 / 점진적 쓰기
  • io.Reader / io.Writer 인터페이스의 의미 이해
  • 경로 다루기와 디렉터리 작업

29.1 파일 열기/닫기

os.Open (읽기 전용)

f, err := os.Open("hello.txt")
if err != nil {
    return err
}
defer f.Close()

// f 로부터 읽기 수행
  • 파일이 없거나 권한이 없으면 에러
  • 성공하면 *os.File 을 돌려준다
  • 다 쓰면 반드시 Close() 해야 자원이 해제된다

os.Create (쓰기 전용, 새 파일)

f, err := os.Create("out.txt")
if err != nil {
    return err
}
defer f.Close()
  • 파일이 없으면 새로 만든다
  • 이미 있으면 기존 내용을 비우고 새로 시작한다
  • 의도와 다르게 기존 파일을 날리지 않도록 주의

os.OpenFile (옵션 지정)

가장 일반적인 형태다. 플래그로 동작을 세밀하게 정한다.

f, err := os.OpenFile(
    "app.log",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY,
    0644,
)

자주 쓰는 플래그:

플래그의미
O_RDONLY읽기 전용
O_WRONLY쓰기 전용
O_RDWR읽기 + 쓰기
O_APPEND끝에 이어 쓰기
O_CREATE없으면 만들기
O_TRUNC열 때 내용을 비움

세 번째 인자는 권한(퍼미션) 이다. 0644 는 “주인 읽기/쓰기, 나머지 읽기만” 이다.

defer f.Close() 관례

9장에서 본 defer 가 여기서 빛을 발한다. 파일을 열자마자 defer f.Close() 를 적어 두면 함수가 어떻게 끝나든 닫힘이 보장된다.

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    // 여기서 어떤 에러로 빠져나가도
    // Close() 는 호출된다
    ...
    return nil
}

닫기 자체에서 발생한 에러를 챙겨야 할 때도 있다. 그럴 때는 명시적으로 호출하고 에러를 살핀다.

에러 처리

21장의 에러 처리 관례를 그대로 따른다.

f, err := os.Open(path)
if err != nil {
    return fmt.Errorf("파일 열기 실패: %w", err)
}
defer f.Close()

%w 로 래핑해서 호출자가 원인을 추적할 수 있게 한다.


29.2 파일 읽기

상황에 따라 세 가지 방식이 있다.

전체 한 번에: os.ReadFile

작은 파일을 통째로 읽을 때.

data, err := os.ReadFile("config.json")
if err != nil {
    return err
}
fmt.Println(string(data))
  • 열기, 읽기, 닫기를 한 번에 처리
  • 결과는 []byte
  • 파일이 크다면 메모리를 통째로 차지하므로 주의 (26장 참고)

io.ReadAll

이미 열린 io.Reader 에서 전체를 읽어 들일 때.

f, err := os.Open("data.txt")
if err != nil {
    return err
}
defer f.Close()

data, err := io.ReadAll(f)

os.ReadFile 과 비슷하지만 “이미 손에 든 Reader” 에 쓸 수 있다는 점이 다르다.

한 줄씩: bufio.Scanner

텍스트 파일을 줄 단위로 처리할 때 표준 패턴이다.

f, err := os.Open("big.log")
if err != nil {
    return err
}
defer f.Close()

sc := bufio.NewScanner(f)
for sc.Scan() {
    line := sc.Text()
    process(line)
}
if err := sc.Err(); err != nil {
    return err
}
  • sc.Scan() 이 true 면 다음 줄이 있다
  • sc.Text() 가 줄 내용 (개행 제외)
  • 끝난 뒤 sc.Err() 로 에러를 확인

기본 줄 길이 제한이 있다. 긴 줄을 다룬다면 sc.Buffer(...) 로 키운다.

청크 단위: io.Reader + 버퍼

바이너리 파일이거나 줄이 의미 없는 경우.

f, _ := os.Open("video.mp4")
defer f.Close()

buf := make([]byte, 4096)
for {
    n, err := f.Read(buf)
    if n > 0 {
        process(buf[:n])
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        return err
    }
}

Read 의 반환 규칙은 약간 까다롭다. n 이 0 이상이고 errio.EOF 일 수 있는 등 모든 조합을 다 처리해야 한다.

귀찮으면 위에서 본 Scannerio.ReadAll 을 쓰는 게 낫다.


29.3 파일 쓰기

전체 한 번에: os.WriteFile

data := []byte("hello\n")
err := os.WriteFile("hello.txt", data, 0644)
  • 파일을 만들거나 덮어쓰고
  • 데이터를 통째로 적고
  • 닫는다

작은 결과를 단번에 저장할 때 편하다.

점진적으로: bufio.Writer

여러 번 나눠서 쓸 때.

f, err := os.Create("out.txt")
if err != nil {
    return err
}
defer f.Close()

w := bufio.NewWriter(f)
for _, line := range lines {
    w.WriteString(line)
    w.WriteByte('\n')
}
if err := w.Flush(); err != nil {
    return err
}

bufio.Writer 는 내부 버퍼에 모아 두었다가 한 번에 파일로 보낸다. 시스템 호출 횟수가 줄어 성능이 좋다.

Flush() 를 잊지 마라. 내부 버퍼에만 남고 파일에 안 적힌 데이터가 그대로 사라질 수 있다.

defer w.Flush() 보다 명시적으로 끝에 Flush() 후 에러 검사를 하는 게 안전하다. defer 에서는 에러를 챙기기 까다롭다.


29.4 io.Reader / io.Writer 인터페이스

이 장에 계속 등장하는 두 인터페이스를 정리한다.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Go 의 입출력은 거의 전부 이 두 인터페이스로 통일된다.

  • os.File 은 둘 다 구현한다
  • net.Conn (네트워크 연결) 도 구현한다
  • bytes.Buffer, strings.Reader 도 구현한다
  • gzip.Reader (압축 해제) 도 구현한다

이게 강력한 이유. io.Copy(dst, src) 같은 함수 하나로 “파일에서 네트워크로”, “압축 해제 후 파일로” 등 어떤 조합이든 처리할 수 있다.

io.Copy(dstFile, srcFile)   // 파일 복사
io.Copy(dstConn, srcFile)   // 파일 → 네트워크
io.Copy(dstFile, gzReader)  // 압축 해제 → 파일

16장의 인터페이스가 이런 식으로 빛을 발한다.


29.5 경로 다루기

문자열을 직접 자르고 합치는 대신 표준 패키지를 쓴다.

path/filepath 패키지

filepath.Join("dir", "sub", "a.txt")
// "dir/sub/a.txt" (Linux/macOS)
// "dir\\sub\\a.txt" (Windows)

filepath.Dir("/a/b/c.txt")  // "/a/b"
filepath.Base("/a/b/c.txt") // "c.txt"
filepath.Ext("c.txt")        // ".txt"

abs, err := filepath.Abs("./data")
// 절대 경로로 변환
함수의미
Join경로 조각을 안전하게 합침
Dir디렉터리 부분
Base파일명 부분
Ext확장자 부분 (점 포함)
Abs절대 경로
Clean.., . 등을 정리

윈도우/유닉스 경로 차이

filepath 는 OS 의 경로 구분자를 자동으로 쓴다.

OS구분자
Linux / macOS/
Windows\

직접 + "/" + 로 합치면 윈도우에서 문제가 생긴다. 항상 filepath.Join 을 쓴다.

URL 경로처럼 항상 / 인 경우는 path 패키지를 쓴다. filepath 와 이름이 비슷하지만 다른 패키지다.


29.6 디렉터리 작업

디렉터리 만들기

os.Mkdir("data", 0755)       // 한 단계
os.MkdirAll("data/2026/05", 0755) // 중간 디렉터리 포함
  • Mkdir 은 부모가 없으면 실패한다
  • MkdirAll 은 중간 경로를 자동으로 만든다

디렉터리 내용 읽기

entries, err := os.ReadDir("data")
if err != nil {
    return err
}
for _, e := range entries {
    fmt.Println(e.Name(), e.IsDir())
}

os.ReadDir 은 디렉터리의 한 단계 자식 목록을 돌려준다. 각 항목에 Name(), IsDir(), Type() 같은 메서드가 있다.

파일 / 디렉터리 정보

info, err := os.Stat("hello.txt")
if err != nil {
    return err
}
fmt.Println(info.Size())    // 바이트 수
fmt.Println(info.ModTime()) // 마지막 수정 시각
fmt.Println(info.IsDir())   // 디렉터리인가

os.Stat 으로 메타데이터를 얻는다. “파일이 존재하는가” 확인에도 쓴다.

if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
    // 파일 없음
}

21장에서 본 errors.Is 와 함께 쓰면 깔끔하다.

파일 트리 순회

전체 디렉터리를 재귀적으로 훑을 때.

err := filepath.WalkDir(
    "data",
    func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        fmt.Println(path)
        return nil
    },
)
  • 모든 파일/디렉터리에 대해 콜백이 호출된다
  • 콜백이 에러를 반환하면 순회가 중단된다
  • 특정 디렉터리를 건너뛰려면 return filepath.SkipDir 를 반환한다

파일/디렉터리 지우기

os.Remove("hello.txt")       // 한 파일
os.RemoveAll("dir/sub")       // 디렉터리 전체 (위험)

RemoveAll 은 강력하다. 경로 계산이 잘못되면 엉뚱한 곳을 다 날릴 수 있다. 넘기는 경로를 두 번 확인하자.


29.7 정리

  • 파일 열기 = os.Open (읽기), os.Create (쓰기), os.OpenFile (옵션)
    • 항상 defer f.Close()
  • 읽기
    • 전체: os.ReadFile, io.ReadAll
    • 줄 단위: bufio.Scanner
    • 청크 단위: io.Reader.Read
  • 쓰기
    • 전체: os.WriteFile
    • 점진적: bufio.Writer (반드시 Flush)
  • io.Reader / io.Writer 는 파일/네트워크/압축 등 모든 입출력을 하나의 모양으로 묶는다
  • 경로는 path/filepath
    • Join, Dir, Base, Ext, Abs
  • 디렉터리는 os.Mkdir, os.ReadDir, os.Stat, filepath.WalkDir

파일 입출력은 패턴이 정형화돼 있다. “열기 → defer 닫기 → 읽기/쓰기 → 에러 검사” 이 흐름만 손에 익으면 대부분의 작업을 해낼 수 있다.

다음 장에서는 데이터를 파일이나 네트워크로 주고받을 때 가장 자주 쓰이는 형식인 JSON 을 다룬다.