[Go] 파일 입/출력
Updated:
개요
파일 입/출력(File I/O)은 데이터를 영구적으로 저장하고 읽어오는 필수적인 기능입니다. Go는 os, io, bufio 패키지를 통해 강력하고 효율적인 파일 처리 기능을 제공합니다.
주요 패키지
- os: 파일 시스템 기본 작업 (생성, 열기, 삭제 등)
- io: 입출력 인터페이스 및 유틸리티
- bufio: 버퍼링된 입출력
- path/filepath: 경로 처리
- io/ioutil: 간단한 I/O 유틸리티 (Go 1.16부터 deprecated, os로 통합)
파일 열기와 닫기
파일 열기
os.Open (읽기 전용)
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("파일 열기 실패:", err)
return
}
defer file.Close() // 함수 종료 시 자동 닫기
fmt.Println("파일 열기 성공")
}
os.OpenFile (플래그 지정)
package main
import (
"fmt"
"os"
)
func main() {
// 읽기/쓰기 모드로 열기, 없으면 생성
file, err := os.OpenFile("test.txt", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
fmt.Println("파일 열기 실패:", err)
return
}
defer file.Close()
fmt.Println("파일 열기 성공")
}
파일 열기 플래그
| 플래그 | 설명 |
|---|---|
os.O_RDONLY |
읽기 전용 |
os.O_WRONLY |
쓰기 전용 |
os.O_RDWR |
읽기/쓰기 |
os.O_CREATE |
파일이 없으면 생성 |
os.O_TRUNC |
파일을 열 때 내용 삭제 |
os.O_APPEND |
파일 끝에 추가 |
os.O_EXCL |
O_CREATE와 함께 사용, 파일이 이미 있으면 에러 |
파일 권한 (Permission)
0644 // rw-r--r-- (소유자: 읽기/쓰기, 그룹/기타: 읽기)
0755 // rwxr-xr-x (소유자: 읽기/쓰기/실행, 그룹/기타: 읽기/실행)
0600 // rw------- (소유자만 읽기/쓰기)
파일 생성
os.Create
package main
import (
"fmt"
"os"
)
func main() {
// 파일 생성 (이미 있으면 내용 삭제)
file, err := os.Create("new_file.txt")
if err != nil {
fmt.Println("파일 생성 실패:", err)
return
}
defer file.Close()
fmt.Println("파일 생성 성공")
}
os.OpenFile로 생성
// 파일이 없을 때만 생성 (이미 있으면 에러)
file, err := os.OpenFile("new_file.txt",
os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
fmt.Println("파일이 이미 존재하거나 생성 실패:", err)
return
}
defer file.Close()
파일 읽기
1. os.ReadFile (간단한 방법, 전체 읽기)
Go 1.16+에서 권장되는 방법
package main
import (
"fmt"
"os"
)
func main() {
// 파일 전체를 바이트 슬라이스로 읽기
data, err := os.ReadFile("test.txt")
if err != nil {
fmt.Println("파일 읽기 실패:", err)
return
}
fmt.Println("파일 내용:")
fmt.Println(string(data))
}
2. File.Read (저수준 읽기)
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("파일 열기 실패:", err)
return
}
defer file.Close()
// 버퍼 생성
buffer := make([]byte, 100)
// 읽기
n, err := file.Read(buffer)
if err != nil {
fmt.Println("읽기 실패:", err)
return
}
fmt.Printf("읽은 바이트 수: %d\n", n)
fmt.Printf("내용: %s\n", string(buffer[:n]))
}
3. bufio.Scanner (라인별 읽기)
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("파일 열기 실패:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 1
// 한 줄씩 읽기
for scanner.Scan() {
line := scanner.Text()
fmt.Printf("%d: %s\n", lineNum, line)
lineNum++
}
// 스캔 에러 확인
if err := scanner.Err(); err != nil {
fmt.Println("스캔 에러:", err)
}
}
4. bufio.Reader (버퍼링된 읽기)
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("파일 열기 실패:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
// 마지막 줄 처리 (줄바꿈 없이 끝날 수 있음)
if len(line) > 0 {
fmt.Print(line)
}
break
}
fmt.Println("읽기 에러:", err)
return
}
fmt.Print(line)
}
}
5. io.ReadAll (전체 읽기)
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
fmt.Println("파일 열기 실패:", err)
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
fmt.Println("읽기 실패:", err)
return
}
fmt.Println(string(data))
}
읽기 방법 비교
| 방법 | 장점 | 단점 | 사용 상황 |
|---|---|---|---|
os.ReadFile |
가장 간단 | 큰 파일 시 메모리 부담 | 작은 파일 전체 읽기 |
File.Read |
세밀한 제어 | 코드 복잡 | 바이너리 파일, 부분 읽기 |
bufio.Scanner |
라인별 처리 용이 | 라인 크기 제한 | 텍스트 파일 라인별 처리 |
bufio.Reader |
효율적, 유연 | 약간 복잡 | 대용량 파일, 스트리밍 |
io.ReadAll |
간단 | 메모리 부담 | 작은 파일, Reader 읽기 |
파일 쓰기
1. os.WriteFile (간단한 방법)
package main
import (
"fmt"
"os"
)
func main() {
data := []byte("Hello, World!\n안녕하세요!\n")
// 파일에 쓰기 (덮어쓰기)
err := os.WriteFile("output.txt", data, 0644)
if err != nil {
fmt.Println("쓰기 실패:", err)
return
}
fmt.Println("파일 쓰기 성공")
}
2. File.Write (저수준 쓰기)
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("파일 생성 실패:", err)
return
}
defer file.Close()
data := []byte("Hello, Go!\n")
n, err := file.Write(data)
if err != nil {
fmt.Println("쓰기 실패:", err)
return
}
fmt.Printf("%d 바이트 쓰기 완료\n", n)
}
3. File.WriteString
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("파일 생성 실패:", err)
return
}
defer file.Close()
lines := []string{
"첫 번째 줄\n",
"두 번째 줄\n",
"세 번째 줄\n",
}
for _, line := range lines {
_, err := file.WriteString(line)
if err != nil {
fmt.Println("쓰기 실패:", err)
return
}
}
fmt.Println("쓰기 완료")
}
4. bufio.Writer (버퍼링된 쓰기)
대량 데이터 쓰기 시 성능 향상
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("파일 생성 실패:", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)
// 여러 줄 쓰기
for i := 1; i <= 1000; i++ {
fmt.Fprintf(writer, "라인 %d\n", i)
}
// 버퍼 비우기 (중요!)
err = writer.Flush()
if err != nil {
fmt.Println("플러시 실패:", err)
return
}
fmt.Println("쓰기 완료")
}
5. 파일에 추가 (Append)
package main
import (
"fmt"
"os"
)
func main() {
// 추가 모드로 열기
file, err := os.OpenFile("output.txt",
os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
fmt.Println("파일 열기 실패:", err)
return
}
defer file.Close()
// 파일 끝에 추가
_, err = file.WriteString("추가된 내용\n")
if err != nil {
fmt.Println("쓰기 실패:", err)
return
}
fmt.Println("추가 완료")
}
6. fmt.Fprintf로 포맷 쓰기
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("파일 생성 실패:", err)
return
}
defer file.Close()
name := "Alice"
age := 30
score := 95.5
fmt.Fprintf(file, "이름: %s\n", name)
fmt.Fprintf(file, "나이: %d\n", age)
fmt.Fprintf(file, "점수: %.1f\n", score)
fmt.Println("쓰기 완료")
}
파일 정보 확인
os.Stat (파일 정보)
package main
import (
"fmt"
"os"
"time"
)
func main() {
fileInfo, err := os.Stat("test.txt")
if err != nil {
fmt.Println("파일 정보 확인 실패:", err)
return
}
fmt.Println("파일명:", fileInfo.Name())
fmt.Println("크기:", fileInfo.Size(), "바이트")
fmt.Println("권한:", fileInfo.Mode())
fmt.Println("수정 시간:", fileInfo.ModTime().Format(time.RFC3339))
fmt.Println("디렉토리 여부:", fileInfo.IsDir())
}
파일 존재 확인
package main
import (
"fmt"
"os"
)
func fileExists(filename string) bool {
_, err := os.Stat(filename)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false // 다른 에러 (권한 등)
}
func main() {
if fileExists("test.txt") {
fmt.Println("파일이 존재합니다.")
} else {
fmt.Println("파일이 존재하지 않습니다.")
}
}
파일 삭제 및 이름 변경
파일 삭제
package main
import (
"fmt"
"os"
)
func main() {
err := os.Remove("test.txt")
if err != nil {
fmt.Println("삭제 실패:", err)
return
}
fmt.Println("파일 삭제 완료")
}
파일 이름 변경/이동
package main
import (
"fmt"
"os"
)
func main() {
// 파일 이름 변경
err := os.Rename("old.txt", "new.txt")
if err != nil {
fmt.Println("이름 변경 실패:", err)
return
}
fmt.Println("이름 변경 완료")
// 파일 이동 (다른 디렉토리로)
err = os.Rename("file.txt", "subdir/file.txt")
if err != nil {
fmt.Println("이동 실패:", err)
return
}
}
디렉토리 작업
디렉토리 생성
package main
import (
"fmt"
"os"
)
func main() {
// 단일 디렉토리 생성
err := os.Mkdir("testdir", 0755)
if err != nil {
fmt.Println("디렉토리 생성 실패:", err)
return
}
// 중첩 디렉토리 생성 (mkdir -p)
err = os.MkdirAll("parent/child/grandchild", 0755)
if err != nil {
fmt.Println("디렉토리 생성 실패:", err)
return
}
fmt.Println("디렉토리 생성 완료")
}
디렉토리 읽기
package main
import (
"fmt"
"os"
)
func main() {
// 디렉토리 엔트리 읽기
entries, err := os.ReadDir(".")
if err != nil {
fmt.Println("디렉토리 읽기 실패:", err)
return
}
fmt.Println("현재 디렉토리 내용:")
for _, entry := range entries {
if entry.IsDir() {
fmt.Printf("[DIR] %s\n", entry.Name())
} else {
fmt.Printf("[FILE] %s\n", entry.Name())
}
}
}
디렉토리 삭제
package main
import (
"fmt"
"os"
)
func main() {
// 빈 디렉토리 삭제
err := os.Remove("testdir")
if err != nil {
fmt.Println("삭제 실패:", err)
return
}
// 디렉토리와 모든 하위 항목 삭제 (rm -rf)
err = os.RemoveAll("parent")
if err != nil {
fmt.Println("삭제 실패:", err)
return
}
fmt.Println("디렉토리 삭제 완료")
}
경로 처리
path/filepath 패키지
package main
import (
"fmt"
"path/filepath"
)
func main() {
// 경로 결합
path := filepath.Join("dir", "subdir", "file.txt")
fmt.Println("경로:", path) // dir/subdir/file.txt (또는 dir\subdir\file.txt)
// 절대 경로 변환
absPath, _ := filepath.Abs("test.txt")
fmt.Println("절대 경로:", absPath)
// 파일명 추출
filename := filepath.Base("/path/to/file.txt")
fmt.Println("파일명:", filename) // file.txt
// 디렉토리 추출
dir := filepath.Dir("/path/to/file.txt")
fmt.Println("디렉토리:", dir) // /path/to
// 확장자 추출
ext := filepath.Ext("file.txt")
fmt.Println("확장자:", ext) // .txt
// 경로 분리
dir, file := filepath.Split("/path/to/file.txt")
fmt.Println("디렉토리:", dir) // /path/to/
fmt.Println("파일:", file) // file.txt
}
경로 패턴 매칭
package main
import (
"fmt"
"path/filepath"
)
func main() {
// 글로브 패턴으로 파일 찾기
matches, err := filepath.Glob("*.txt")
if err != nil {
fmt.Println("에러:", err)
return
}
fmt.Println("*.txt 파일들:")
for _, match := range matches {
fmt.Println(match)
}
// 재귀적으로 파일 찾기
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".go" {
fmt.Println("Go 파일:", path)
}
return nil
})
}
실용 예제
예제 1: 파일 복사
package main
import (
"fmt"
"io"
"os"
)
func copyFile(src, dst string) error {
// 원본 파일 열기
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
// 대상 파일 생성
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
// 복사
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}
// 버퍼 플러시
return destFile.Sync()
}
func main() {
err := copyFile("source.txt", "destination.txt")
if err != nil {
fmt.Println("복사 실패:", err)
return
}
fmt.Println("파일 복사 완료")
}
예제 2: CSV 파일 읽기/쓰기
package main
import (
"encoding/csv"
"fmt"
"os"
)
func writeCSV() error {
file, err := os.Create("data.csv")
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// 헤더
writer.Write([]string{"이름", "나이", "도시"})
// 데이터
records := [][]string{
{"Alice", "30", "Seoul"},
{"Bob", "25", "Busan"},
{"Charlie", "35", "Incheon"},
}
for _, record := range records {
if err := writer.Write(record); err != nil {
return err
}
}
return nil
}
func readCSV() error {
file, err := os.Open("data.csv")
if err != nil {
return err
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return err
}
for i, record := range records {
fmt.Printf("Row %d: %v\n", i, record)
}
return nil
}
func main() {
if err := writeCSV(); err != nil {
fmt.Println("쓰기 에러:", err)
return
}
if err := readCSV(); err != nil {
fmt.Println("읽기 에러:", err)
return
}
}
예제 3: JSON 파일 처리
package main
import (
"encoding/json"
"fmt"
"os"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
func writeJSON() error {
people := []Person{
{"Alice", 30, "alice@example.com"},
{"Bob", 25, "bob@example.com"},
}
file, err := os.Create("people.json")
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ") // 보기 좋게 포맷
return encoder.Encode(people)
}
func readJSON() error {
file, err := os.Open("people.json")
if err != nil {
return err
}
defer file.Close()
var people []Person
decoder := json.NewDecoder(file)
if err := decoder.Decode(&people); err != nil {
return err
}
for _, p := range people {
fmt.Printf("%s (%d세): %s\n", p.Name, p.Age, p.Email)
}
return nil
}
func main() {
if err := writeJSON(); err != nil {
fmt.Println("JSON 쓰기 에러:", err)
return
}
if err := readJSON(); err != nil {
fmt.Println("JSON 읽기 에러:", err)
return
}
}
예제 4: 로그 파일 작성
package main
import (
"fmt"
"os"
"time"
)
type Logger struct {
file *os.File
}
func NewLogger(filename string) (*Logger, error) {
file, err := os.OpenFile(filename,
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
return &Logger{file: file}, nil
}
func (l *Logger) Log(message string) error {
timestamp := time.Now().Format("2006-01-02 15:04:05")
logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
_, err := l.file.WriteString(logEntry)
return err
}
func (l *Logger) Close() error {
return l.file.Close()
}
func main() {
logger, err := NewLogger("app.log")
if err != nil {
fmt.Println("로거 생성 실패:", err)
return
}
defer logger.Close()
logger.Log("애플리케이션 시작")
logger.Log("사용자 로그인: Alice")
logger.Log("데이터 처리 완료")
logger.Log("애플리케이션 종료")
fmt.Println("로그 작성 완료")
}
예제 5: 대용량 파일 처리
package main
import (
"bufio"
"fmt"
"os"
)
func processLargeFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
// 큰 라인을 위한 버퍼 크기 증가
const maxCapacity = 1024 * 1024 // 1MB
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
lineCount := 0
wordCount := 0
for scanner.Scan() {
lineCount++
line := scanner.Text()
// 간단한 단어 수 계산
inWord := false
for _, char := range line {
if char == ' ' || char == '\t' || char == '\n' {
inWord = false
} else if !inWord {
wordCount++
inWord = true
}
}
// 진행 상황 출력
if lineCount%10000 == 0 {
fmt.Printf("처리 중: %d 라인\n", lineCount)
}
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Printf("\n총 라인 수: %d\n", lineCount)
fmt.Printf("총 단어 수: %d\n", wordCount)
return nil
}
func main() {
err := processLargeFile("large_file.txt")
if err != nil {
fmt.Println("처리 실패:", err)
}
}
에러 처리
파일 에러 유형
package main
import (
"errors"
"fmt"
"os"
)
func main() {
_, err := os.Open("nonexistent.txt")
if err != nil {
if os.IsNotExist(err) {
fmt.Println("파일이 존재하지 않습니다.")
} else if os.IsPermission(err) {
fmt.Println("권한이 없습니다.")
} else if errors.Is(err, os.ErrClosed) {
fmt.Println("파일이 이미 닫혔습니다.")
} else {
fmt.Println("알 수 없는 에러:", err)
}
}
}
안전한 파일 쓰기 (원자적 쓰기)
package main
import (
"fmt"
"os"
"path/filepath"
)
func safeWriteFile(filename string, data []byte) error {
// 임시 파일에 쓰기
dir := filepath.Dir(filename)
tmpFile, err := os.CreateTemp(dir, "tmp-")
if err != nil {
return err
}
tmpName := tmpFile.Name()
defer func() {
tmpFile.Close()
os.Remove(tmpName) // 정리
}()
// 데이터 쓰기
if _, err := tmpFile.Write(data); err != nil {
return err
}
// 디스크에 동기화
if err := tmpFile.Sync(); err != nil {
return err
}
// 파일 닫기
if err := tmpFile.Close(); err != nil {
return err
}
// 원자적으로 이름 변경
return os.Rename(tmpName, filename)
}
func main() {
data := []byte("중요한 데이터\n")
err := safeWriteFile("important.txt", data)
if err != nil {
fmt.Println("쓰기 실패:", err)
return
}
fmt.Println("안전하게 저장 완료")
}
임시 파일
os.CreateTemp
package main
import (
"fmt"
"os"
)
func main() {
// 임시 파일 생성
tmpFile, err := os.CreateTemp("", "example-*.txt")
if err != nil {
fmt.Println("임시 파일 생성 실패:", err)
return
}
defer os.Remove(tmpFile.Name()) // 정리
defer tmpFile.Close()
fmt.Println("임시 파일:", tmpFile.Name())
// 임시 파일에 쓰기
if _, err := tmpFile.WriteString("임시 데이터\n"); err != nil {
fmt.Println("쓰기 실패:", err)
return
}
fmt.Println("임시 파일 사용 완료")
}
os.MkdirTemp (임시 디렉토리)
package main
import (
"fmt"
"os"
)
func main() {
tmpDir, err := os.MkdirTemp("", "example-")
if err != nil {
fmt.Println("임시 디렉토리 생성 실패:", err)
return
}
defer os.RemoveAll(tmpDir) // 정리
fmt.Println("임시 디렉토리:", tmpDir)
// 임시 디렉토리 사용
// ...
}
일반적인 실수
1. defer Close() 누락
// ❌ 나쁜 예
file, _ := os.Open("file.txt")
// Close() 호출 없음 - 리소스 누수!
// ✅ 좋은 예
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer file.Close()
2. 에러 무시
// ❌ 나쁜 예
data, _ := os.ReadFile("file.txt")
// ✅ 좋은 예
data, err := os.ReadFile("file.txt")
if err != nil {
log.Fatal(err)
}
3. bufio.Writer Flush 누락
// ❌ 나쁜 예
writer := bufio.NewWriter(file)
writer.WriteString("data")
// Flush() 없음 - 데이터 손실 가능!
// ✅ 좋은 예
writer := bufio.NewWriter(file)
writer.WriteString("data")
writer.Flush()
4. 경로 구분자 하드코딩
// ❌ 나쁜 예 (Windows에서 문제)
path := "dir/subdir/file.txt"
// ✅ 좋은 예
path := filepath.Join("dir", "subdir", "file.txt")
5. 큰 파일 전체 읽기
// ❌ 나쁜 예 - 메모리 부족 가능
data, _ := os.ReadFile("large_file.txt")
// ✅ 좋은 예 - 스트리밍 처리
file, _ := os.Open("large_file.txt")
scanner := bufio.NewScanner(file)
for scanner.Scan() {
// 라인별 처리
}
성능 고려사항
1. 버퍼링 사용
// 느림
for i := 0; i < 10000; i++ {
file.WriteString(fmt.Sprintf("Line %d\n", i))
}
// 빠름
writer := bufio.NewWriter(file)
for i := 0; i < 10000; i++ {
fmt.Fprintf(writer, "Line %d\n", i)
}
writer.Flush()
2. 적절한 버퍼 크기
// 기본 버퍼 (4096 바이트)
reader := bufio.NewReader(file)
// 큰 버퍼 (더 나은 성능)
reader := bufio.NewReaderSize(file, 32*1024)
3. io.Copy 활용
// 직접 읽기/쓰기보다 효율적
io.Copy(destFile, sourceFile)
베스트 프랙티스
- 항상 에러 확인: 모든 파일 작업 후 에러 체크
- defer로 리소스 정리:
defer file.Close()패턴 사용 - bufio 활용: 대량 데이터는 버퍼링된 I/O 사용
- filepath 패키지: 크로스 플랫폼 경로 처리
- 적절한 권한: 파일 생성 시 최소 권한 원칙
- 원자적 쓰기: 중요한 데이터는 임시 파일 → 이름 변경
- 스트리밍 처리: 큰 파일은 전체 읽기 대신 라인별 처리
- 에러 타입 확인:
os.IsNotExist,os.IsPermission활용 - 임시 파일 정리:
defer os.Remove()사용 - 동기화: 중요 데이터는
Sync()호출로 디스크 동기화 - 상대 경로 주의: 절대 경로 또는
filepath.Abs사용 고려 - 파일 잠금: 필요시 OS별 파일 잠금 메커니즘 사용