Menu

Golang 파일 읽기와 쓰기: os, bufio, 디렉터리

Go에서 파일을 읽고 쓰는 방법: os.ReadFile과 os.WriteFile, bufio.Scanner로 한 줄씩 읽기, os.OpenFile로 이어 쓰기, 파일이 있는지 확인하기, 디렉터리 다루기를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

파일 전체를 읽고 쓰기

os.WriteFileos.ReadFile로 대부분의 필요를 해결할 수 있습니다. 한 번의 호출로 열고, 쓰거나 읽고, 닫습니다.

이 페이지의 예제는 os.MkdirTemp로 만든 임시 디렉터리에서 작업하고 defer os.RemoveAll(dir)로 지우므로 아무것도 남기지 않습니다. 여러분의 코드에서 "config.json" 같은 상대 경로는 프로세스의 작업 디렉터리를 기준으로 해석되며, 그곳이 소스 파일이나 바이너리의 디렉터리라는 보장은 없습니다.

os.WriteFile은 필요하면 파일을 만들고, 이미 있으면 내용을 비웁니다. 세 번째 인자는 새로 만드는 파일의 Unix 권한입니다. 0o644는 소유자가 읽고 쓸 수 있고 나머지는 읽을 수 있다는 뜻입니다. 이미 있는 파일에는 무시되고, 프로세스의 umask가 일부 비트를 지울 수 있습니다.

os.ReadFile은 전부를 메모리로 읽습니다. 설정 파일과 작은 입력에는 알맞지만, 몇 기가바이트짜리 로그에는 맞지 않습니다.

bufio.Scanner로 한 줄씩 읽기

큰 파일이거나 어차피 줄 단위가 필요하다면 bufio.Scanner를 쓰세요. 덩어리로 읽어서 줄바꿈을 뺀 한 줄씩 건네줍니다.

세 가지 세부 사항:

  • 반복이 끝나면 sc.Err()를 확인하세요. Scan은 파일 끝에서도, 오류가 났을 때도 false를 반환하며, 둘을 구분하는 것은 Err뿐입니다.
  • 64KB 줄 길이 제한. 기본적으로 64KB보다 긴 줄 하나가 있으면 스캐너가 bufio.Scanner: token too long으로 멈춥니다. 줄이 긴 파일(압축된 JSON, 일부 로그)이라면 반복 전에 제한을 올리세요: sc.Buffer(make([]byte, 1024*1024), 10*1024*1024).
  • 다른 단위. sc.Split(bufio.ScanWords)는 단어를, bufio.ScanRunes는 문자를 내놓습니다.

줄이 아니라 고정 크기 덩어리로 스트림을 읽으려면 반복문에서 f.Read(buf)를 쓰거나 io.Copy로 다른 writer에 넘기세요.

쓰기: os.Create, os.OpenFile, 이어 쓰기

os.Create(name)은 쓰기용으로 파일을 열면서 만들거나 비웁니다. os.OpenFile은 플래그로 모든 것을 제어합니다:

플래그의미
os.O_RDONLY, os.O_WRONLY, os.O_RDWR읽기, 쓰기, 또는 둘 다를 위해 열기(하나 선택)
os.O_CREATE파일이 없으면 만들기
os.O_TRUNC열 때 파일 비우기
os.O_APPEND모든 쓰기를 끝에 붙이기
os.O_EXCLO_CREATE와 함께: 파일이 이미 있으면 실패

os.Open(name)OpenFile(name, O_RDONLY, 0)이고, os.Create(name)OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0o666)입니다.

쓸 때는 Close의 오류가 중요합니다. 어떤 파일 시스템은 쓰기 실패를 닫을 때만 보고하므로, defer f.Close()만으로는 잃어버린 데이터를 숨길 수 있습니다. 쓰는 파일이라면 appendLine처럼 Close를 명시적으로 확인하세요. 읽기만 하는 파일이라면 defer f.Close()로 충분합니다.

fmt.Fprintln을 비롯한 fmt 출력 함수들은 파일을 포함한 모든 io.Writer를 받습니다. bufio.Writer는 작은 쓰기를 메모리에 모아서 처리합니다. w.Flush()를 잊는 것은 전형적인 버그입니다. 프로그램은 정상 종료되는데 마지막 몇 킬로바이트가 파일에 도달하지 않습니다.

파일이 있는가?

Go에는 os.Exists가 없습니다. os.Stat을 호출하고 오류를 살펴보세요:

errors.Is(err, fs.ErrNotExist)가 현재의 관용구입니다. 감싼 오류를 꿰뚫어 보지 못하는 예전의 os.IsNotExist(err)를 대체합니다.

열기 전에 확인하는 것은 불필요하고 경쟁 상태를 만드는 경우가 많습니다. 확인과 열기 사이에 파일이 생기거나 사라질 수 있기 때문입니다. 보통은 그냥 열고, 열기에서 나온 fs.ErrNotExist를 처리하면 됩니다. 파일이 아직 없을 때만 만들려면 O_CREATE|O_EXCL을 쓰세요. 확인과 생성이 원자적인 한 단계가 됩니다.

디렉터리

작업함수
디렉터리 하나 만들기os.Mkdir(path, 0o755)
부모까지 포함한 경로 만들기os.MkdirAll(path, 0o755)
디렉터리 목록os.ReadDir(path)
트리 순회filepath.WalkDir(root, fn)
파일이나 빈 디렉터리 삭제os.Remove(path)
트리 삭제os.RemoveAll(path)
이름 바꾸기나 이동os.Rename(old, new)
임시 파일이나 디렉터리os.CreateTemp("", "prefix-*"), os.MkdirTemp("", "prefix")
경로 조각 합치기filepath.Join(a, b, c)

파일 시스템 경로에는 path/filepath를 쓰세요. 운영체제에 맞는 구분자(Windows에서는 \)를 씁니다. path 패키지는 URL처럼 슬래시로 구분된 경로용입니다.

Go 1.24에는 os.Root(os.OpenRoot(dir))도 추가되었습니다. 한 디렉터리 안의 파일만 열고, ..나 심볼릭 링크로 빠져나가는 경로는 거부합니다. 파일 이름이 사용자에게서 올 때 쓰세요.

흔한 실수

  • 오류를 확인하지 않음. 이 호출들은 모두 실패할 수 있습니다. 열기에 실패한 파일은 nil이고, 이후의 Read, Write, Close는 모두 invalid argument를 반환하므로 진짜 원인(파일이 없었거나 권한이 거부됨)이 가려집니다.
  • 스캔 반복 뒤에 sc.Err()를 잊음. 읽기 오류가 파일 끝처럼 보입니다.
  • bufio.WriterFlush를 잊음. 파일의 끝부분이 빠집니다.
  • 쓰기 후 Close의 오류를 무시함. 쓰기 오류는 거기서만 드러날 수 있습니다.
  • 많은 파일을 도는 반복문 안의 defer f.Close(). 함수가 반환될 때까지 파일이 열린 채로 남아 파일 디스크립터가 바닥날 수 있습니다. 본문을 함수로 옮겨서 반복마다 파일이 닫히게 하세요.
  • 권한을 10진수로 씀. 6440o644가 아닙니다. Go는 644를 10진수로 읽으므로 0o1204가 되어 이상한 비트가 설정됩니다.

자주 묻는 질문

Go에서 파일 전체를 문자열로 읽으려면 어떻게 하나요?

data, err := os.ReadFile("notes.txt")는 내용을 []byte로 반환하며, string(data)로 변환합니다. 파일을 열고, 읽고, 닫는 일을 대신 해 줍니다. 메모리에 넉넉히 들어가는 파일에 쓰고, 큰 파일은 bufio.Scanner로 한 줄씩 읽으세요.

Go에서 파일을 한 줄씩 읽으려면 어떻게 하나요?

os.Open으로 파일을 열고, defer f.Close()를 쓰고, bufio.NewScanner(f)로 감싸서 for sc.Scan() { line := sc.Text() }로 반복한 뒤, 반복이 끝나면 sc.Err()를 확인합니다. 64KB보다 긴 줄은 sc.Buffer로 버퍼를 늘리지 않으면 스캐너가 token too long으로 실패합니다.

Go에서 파일에 이어 쓰려면 어떻게 하나요?

os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)로 열어서 쓰고, Close의 오류를 확인합니다. O_CREATE는 파일이 없으면 만들고, O_APPEND는 모든 쓰기가 끝에 붙게 합니다.

Go에서 파일이 있는지 어떻게 확인하나요?

os.Stat(path)를 호출하고 errors.Is(err, fs.ErrNotExist)로 오류를 검사합니다. 오류가 nil이면 존재하는 것입니다. 다른 오류(예: permission denied)는 알 수 없다는 뜻이므로 "없음"으로 취급하지 말고 따로 처리하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기