Skip to content

파일

Go 언어의 파일 처리 표준 라이브러리는 대략 다음과 같습니다:

  • os 라이브러리, OS 파일 시스템 상호작용의 구체적인 구현을 담당
  • io 라이브러리, IO 읽기/쓰기의 추상화 계층
  • fs 라이브러리, 파일 시스템의 추상화 계층

본 문서에서는 Go 언어를 통한 기본적인 파일 처리 방법을 설명합니다.

열기

파일을 여는 일반적인 두 가지 방법은 os 패키지에서 제공하는 두 가지 함수를 사용하는 것입니다. Open 함수는 파일 포인터와 에러를 반환합니다.

go
func Open(name string) (*File, error)

후자의 OpenFile 은 더욱 세밀한 제어를 제공할 수 있으며, 함수 OpenOpenFile 함수를 간단히 래핑한 것입니다.

go
func OpenFile(name string, flag int, perm FileMode) (*File, error)

먼저 첫 번째 사용 방법을 소개하겠습니다. 파일 이름만 제공하면 됩니다. 코드는 다음과 같습니다.

go
func main() {
   file, err := os.Open("README.txt")
   fmt.Println(file, err)
}

파일 검색 경로는 기본적으로 프로젝트 go.mod 파일이 있는 경로입니다. 프로젝트下に README.txt 파일이 없으므로 자연스럽게 에러가 반환됩니다.

<nil> open README.txt: The system cannot find the file specified.

IO 에러 유형은 다양하므로 파일 존재 여부를 수동으로 판단해야 합니다. 마찬가지로 os 패키지도 이를 위한 편의 함수를 제공합니다. 수정된 코드는 다음과 같습니다.

go
func main() {
  file, err := os.Open("README.txt")
  if os.IsNotExist(err) {
    fmt.Println("파일이 존재하지 않습니다")
  } else if err != nil {
    fmt.Println("파일 접근 오류")
  } else {
    fmt.Println("파일 읽기 성공", file)
  }
}

다시 실행하면 출력은 다음과 같습니다.

파일이 존재하지 않습니다

실제로 첫 번째 함수로 읽은 파일은 읽기 전용일 뿐이며 수정할 수 없습니다.

go
func Open(name string) (*File, error) {
  return OpenFile(name, O_RDONLY, 0)
}

OpenFile 함수를 사용하면 파일 디스크립터와 파일 권한 등 더 많은 세부 사항을 제어할 수 있습니다. 파일 디스크립터에 대해 os 패키지는 다음과 같은 상수를 제공합니다.

go
const (
   // 읽기 전용, 쓰기 전용, 읽기/쓰기 세 가지 중 하나를 반드시 지정
   O_RDONLY int = syscall.O_RDONLY // 읽기 전용 모드로 파일 열기
   O_WRONLY int = syscall.O_WRONLY // 쓰기 전용 모드로 파일 열기
   O_RDWR   int = syscall.O_RDWR   // 읽기/쓰기 모드로 파일 열기
   // 나머지 값은 동작 제어에 사용
   O_APPEND int = syscall.O_APPEND // 파일 작성 시 데이터 파일 끝에 추가
   O_CREATE int = syscall.O_CREAT  // 파일이 없으면 생성
   O_EXCL   int = syscall.O_EXCL   // O_CREATE 와 함께 사용, 파일은 반드시 없어야 함
   O_SYNC   int = syscall.O_SYNC   // 동기 IO 방식으로 파일 열기
   O_TRUNC  int = syscall.O_TRUNC  // 열기 시 쓰기 가능한 파일 잘라내기
)

파일 권한에 대해서는 다음과 같은 상수를 제공합니다.

go
const (
   ModeDir        = fs.ModeDir        // d: 디렉토리
   ModeAppend     = fs.ModeAppend     // a: 추가만 가능
   ModeExclusive  = fs.ModeExclusive  // l: 전용
   ModeTemporary  = fs.ModeTemporary  // T: 임시 파일
   ModeSymlink    = fs.ModeSymlink    // L: 심볼릭 링크
   ModeDevice     = fs.ModeDevice     // D: 장치 파일
   ModeNamedPipe  = fs.ModeNamedPipe  // p: 명명 파이프 (FIFO)
   ModeSocket     = fs.ModeSocket     // S: Unix 도메인 소켓
   ModeSetuid     = fs.ModeSetuid     // u: setuid
   ModeSetgid     = fs.ModeSetgid     // g: setgid
   ModeCharDevice = fs.ModeCharDevice // c: Unix 문자 장치, ModeDevice 설정 필요
   ModeSticky     = fs.ModeSticky     // t: 스티키 비트
   ModeIrregular  = fs.ModeIrregular  // ?: 비정규 파일

   // 타입 비트 마스크. 일반 파일의 경우 아무것도 설정되지 않음.
   ModeType = fs.ModeType

   ModePerm = fs.ModePerm // Unix 권한 비트, 0o777
)

다음은 읽기/쓰기 모드로 파일을 여는 코드 예제로, 권한은 0666 으로 모든 사람이 파일에 읽기/쓰기를 할 수 있으며, 없을 경우 자동으로 생성됩니다.

go
func main() {
  file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
  if os.IsNotExist(err) {
    fmt.Println("파일이 존재하지 않습니다")
  } else if err != nil {
    fmt.Println("파일 접근 오류")
  } else {
    fmt.Println("파일 열기 성공", file.Name())
    file.Close()
  }
}

출력은 다음과 같습니다.

파일 열기 성공 README.txt

파일 정보를 얻기만 하고 파일은 읽지 않으려면 os.Stat() 함수를 사용할 수 있습니다. 코드 예제는 다음과 같습니다.

go
func main() {
  fileInfo, err := os.Stat("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(fmt.Sprintf("%+v", fileInfo))
  }
}

출력은 다음과 같습니다.

&{name:README.txt FileAttributes:32 CreationTime:{LowDateTime:3603459389 HighDateTime:31016791} LastAccessTime:{LowDateTime:3603459389 HighDateTime:31016791} LastWriteTime:{LowDateTime:3603459389 HighDateTime:31016791} FileSizeHigh
:0 FileSizeLow:0 Reserved0:0 filetype:0 Mutex:{state:0 sema:0} path:README.txt vol:0 idxhi:0 idxlo:0 appendNameToPath:false}

WARNING

파일을 연 후에는 반드시 파일을 닫아야 합니다. 일반적으로 닫기 작업은 defer 문에 배치합니다.

go
defer file.Close()

읽기

파일을 성공적으로 연 후에는 읽기 작업을 수행할 수 있습니다. 파일 읽기 작업에 대해 *os.File 타입은 다음과 같은 공개 메서드를 제공합니다.

go
// 파일을传入된 바이트 슬라이스에 읽기
func (f *File) Read(b []byte) (n int, err error)

// 첫 번째 방법에 비해 지정된 오프셋에서 읽을 수 있음
func (f *File) ReadAt(b []byte, off int64) (n int, err error)

대부분의 경우 첫 번째 방법을 더 많이 사용합니다. 첫 번째 방법의 경우 읽기 시 슬라이스의 동적 확장을 위한 로직을 직접 작성해야 합니다. 코드는 다음과 같습니다.

go
func ReadFile(file *os.File) ([]byte, error) {
  buffer := make([]byte, 0, 512)
  for {
    // 용량이 부족할 때
    if len(buffer) == cap(buffer) {
      // 확장
      buffer = append(buffer, 0)[:len(buffer)]
    }
    // 계속 파일 읽기
    offset, err := file.Read(buffer[len(buffer):cap(buffer)])
    // 기록된 데이터를 슬라이스에 포함
    buffer = buffer[:len(buffer)+offset]
    // 에러 발생 시
    if err != nil {
      if errors.Is(err, io.EOF) {
        err = nil
      }
      return buffer, err
    }
  }
}

나머지 로직은 다음과 같습니다.

go
func main() {
   file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
   if err != nil {
      fmt.Println("파일 접근 오류")
   } else {
      fmt.Println("파일 열기 성공", file.Name())
      bytes, err := ReadFile(file)
      if err != nil {
         fmt.Println("파일 읽기 오류", err)
      } else {
         fmt.Println(string(bytes))
      }
      file.Close()
   }
}

출력은 다음과 같습니다.

파일 열기 성공 README.txt
hello world!

그 외에도 두 가지 편의 함수를 사용하여 파일을 읽을 수 있습니다. 각각 os 패키지의 ReadFile 함수와 io 패키지의 ReadAll 함수입니다. os.ReadFile 의 경우 파일 경로만 제공하면 되며, io.ReadAll 의 경우 io.Reader 타입 구현을 제공해야 합니다.

os.ReadFile

go
func ReadFile(name string) ([]byte, error)

사용 예제는 다음과 같습니다.

go
func main() {
  bytes, err := os.ReadFile("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(string(bytes))
  }
}

출력은 다음과 같습니다.

hello world!

io.ReadAll

go
func ReadAll(r Reader) ([]byte, error)

사용 예제는 다음과 같습니다.

go
func main() {

   file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
   if err != nil {
      fmt.Println("파일 접근 오류")
   } else {
      fmt.Println("파일 열기 성공", file.Name())
      bytes, err := io.ReadAll(file)
      if err != nil {
         fmt.Println(err)
      } else {
         fmt.Println(string(bytes))
      }
      file.Close()
   }
}
파일 열기 성공 README.txt
hello world!

쓰기

os.File 구조체는 데이터 쓰기를 위해 다음과 같은 메서드를 제공합니다.

go
// 바이트 슬라이스 쓰기
func (f *File) Write(b []byte) (n int, err error)

// 문자열 쓰기
func (f *File) WriteString(s string) (n int, err error)

// 지정된 위치에서 쓰기 시작, os.O_APPEND 모드로 열 때 에러 반환
func (f *File) WriteAt(b []byte, off int64) (n int, err error)

파일에 데이터를 쓰려면 O_WRONLY 또는 O_RDWR 모드로 열어야 하며, 그렇지 않으면 파일 쓰기에 실패합니다. 다음은 os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC 모드로 파일을 열고 권한을 0666 으로 지정하여 데이터를 쓰는 예제입니다.

go
func main() {
  file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
  if err != nil {
    fmt.Println("파일 접근 오류")
  } else {
    fmt.Println("파일 열기 성공", file.Name())
    for i := 0; i < 5; i++ {
      offset, err := file.WriteString("hello world!\n")
      if err != nil {
        fmt.Println(offset, err)
      }
    }
    fmt.Println(file.Close())
  }
}

os.O_APPEND 모드로 파일을 열었으므로 파일 작성 시 데이터가 파일 끝에 추가됩니다. 실행 완료 후 파일 내용은 다음과 같습니다.

txt
hello world!
hello world!
hello world!
hello world!
hello world!

파일에 바이트 슬라이스를 쓰는 것도 유사한 작업이므로 더 이상 설명하지 않습니다. 쓰기 작업에 대해 표준 라이브러리는 편의 함수를 제공합니다. 각각 os.WriteFileio.WriteString 입니다.

os.WriteFile

go
func WriteFile(name string, data []byte, perm FileMode) error

사용 예제는 다음과 같습니다.

go
func main() {
  err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
  if err != nil {
    fmt.Println(err)
  }
}

이때 파일 내용은 다음과 같습니다.

txt
hello world!

io.WriteString

go
func WriteString(w Writer, s string) (n int, err error)

사용 예제는 다음과 같습니다.

go
func main() {
   file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
   if err != nil {
      fmt.Println("파일 접근 오류")
   } else {
      fmt.Println("파일 열기 성공", file.Name())
      for i := 0; i < 5; i++ {
         offset, err := io.WriteString(file, "hello world!\n")
         if err != nil {
            fmt.Println(offset, err)
         }
      }
      fmt.Println(file.Close())
   }
}
hello world!
hello world!
hello world!
hello world!
hello world!

함수 os.Create 는 파일 생성에 사용되며, 본질적으로 OpenFile 을 래핑한 것입니다.

go
func Create(name string) (*File, error) {
   return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

WARNING

파일을 생성할 때 부모 디렉토리가 존재하지 않으면 생성에 실패하고 에러가 반환됩니다.

복사

파일을 복사하려면 두 개의 파일을 동시에 열어야 합니다. 첫 번째 방법은 원본 파일의 데이터를 읽은 후 대상 파일에 쓰는 것입니다. 코드 예제는 다음과 같습니다.

go
func main() {
    // 원본 파일에서 데이터 읽기
  data, err := os.ReadFile("README.txt")
  if err != nil {
    fmt.Println(err)
    return
  }
    // 대상 파일에 쓰기
  err = os.WriteFile("README(1).txt", data, 0666)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("복사 성공")
  }
}

*os.File.ReadFrom

다른 방법은 os.File 에서 제공하는 ReadFrom 메서드를 사용하는 것입니다. 파일을 열 때 하나는 읽기 전용, 다른 하나는 쓰기 전용으로 엽니다.

go
func (f *File) ReadFrom(r io.Reader) (n int64, err error)

사용 예제는 다음과 같습니다.

go
func main() {
  // 원본 파일을 읽기 전용으로 열기
  origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer origin.Close()
  // 복사본 파일을 쓰기 전용으로 열기
  target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer target.Close()
  // 원본 파일에서 데이터 읽어서 복사본 파일에 쓰기
  offset, err := target.ReadFrom(origin)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println("파일 복사 성공", offset)
}

이 복사 방식은 원본 파일의 전체 내용을 먼저 메모리에 읽은 후 대상 파일에 쓰므로, 파일이 특히 클 때는 이 방법을 권장하지 않습니다.

io.Copy

다른 방법은 io.Copy 함수를 사용하는 것입니다. 이 함수는 읽으면서 동시에 쓰며, 먼저 내용을 버퍼에 읽은 후 대상 파일에 씁니다. 버퍼의 기본 크기는 32KB 입니다.

go
func Copy(dst Writer, src Reader) (written int64, err error)

사용 예제는 다음과 같습니다.

go
func main() {
  // 원본 파일을 읽기 전용으로 열기
  origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer origin.Close()
  // 복사본 파일을 쓰기 전용으로 열기
  target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer target.Close()
  // 복사
  written, err := io.Copy(target, origin)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(written)
  }
}

io.CopyBuffer 를 사용하여 버퍼 크기를 지정할 수도 있습니다.

이름 바꾸기

이름 바꾸기는 파일 이동으로도 이해할 수 있으며, os 패키지의 Rename 함수를 사용합니다.

go
func Rename(oldpath, newpath string) error

예제는 다음과 같습니다.

go
func main() {
  err := os.Rename("README.txt", "readme.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("이름 바꾸기 성공")
  }
}

이 함수는 디렉토리에도 동일하게 적용됩니다.

삭제

삭제 작업은 다른 작업에 비해 간단하며, os 패키지의 두 가지 함수만 사용합니다.

go
// 단일 파일 또는 빈 디렉토리 삭제, 디렉토리가 비어 있지 않으면 에러 반환
func Remove(name string) error

// 지정된 디렉토리의 모든 파일과 디렉토리 (하위 디렉토리와 하위 파일 포함) 삭제
func RemoveAll(path string) error

사용법은 매우 간단합니다. 다음은 디렉토리를 삭제하는 예제입니다.

go
func main() {
  // 현재 디렉토리의 모든 파일과 하위 디렉토리 삭제
  err := os.RemoveAll(".")
  if err != nil {
    fmt.Println(err)
  }else {
    fmt.Println("삭제 성공")
  }
}

다음은 단일 파일을 삭제하는 예제입니다.

go
func main() {
  // 현재 디렉토리의 파일 삭제
  err := os.Remove("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("삭제 성공")
  }
}

플러시

os.Sync 는 하위 시스템 호출 Fsync 을 래핑한 함수로, 운영체제의 캐시된 IO 를 디스크에 기록하는 데 사용됩니다.

go
func main() {
  create, err := os.Create("test.txt")
  if err != nil {
    panic(err)
  }
  defer create.Close()

  _, err = create.Write([]byte("hello"))
  if err != nil {
    panic(err)
  }

    // 디스크에 기록
  if err := create.Sync();err != nil {
    return
  }
}

디렉토리

디렉토리의 많은 작업은 파일 작업과 유사합니다.

읽기

디렉토리의 경우 두 가지 열기 방법이 있습니다.

os.ReadDir

첫 번째 방법은 os.ReadDir 함수를 사용하는 것입니다.

go
func ReadDir(name string) ([]DirEntry, error)
go
func main() {
   // 현재 디렉토리
   dir, err := os.ReadDir(".")
   if err != nil {
      fmt.Println(err)
   } else {
      for _, entry := range dir {
         fmt.Println(entry.Name())
      }
   }
}

*os.File.ReadDir

두 번째 방법은 *os.File.ReadDir 함수를 사용하는 것입니다. os.ReadDir 은 본질적으로 *os.File.ReadDir 을 간단히 래핑한 것입니다.

go
// n < 0 일 때 디렉토리의 모든 내용 읽기
func (f *File) ReadDir(n int) ([]DirEntry, error)
go
func main() {
   // 현재 디렉토리
   dir, err := os.Open(".")
   if err != nil {
      fmt.Println(err)
   }
   defer dir.Close()
   dirs, err := dir.ReadDir(-1)
   if err != nil {
      fmt.Println(err)
   } else {
      for _, entry := range dirs {
         fmt.Println(entry.Name())
      }
   }
}

생성

디렉토리 생성 작업에는 os 패키지의 두 가지 함수를 사용합니다.

go
// 지정된 권한으로 지정된 이름의 디렉토리 생성
func Mkdir(name string, perm FileMode) error

// 전자에 비해 이 함수는 모든 필요한 부모 디렉토리를 생성
func MkdirAll(path string, perm FileMode) error

예제는 다음과 같습니다.

go
func main() {
  err := os.Mkdir("src", 0666)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("생성 성공")
  }
}

복사

디렉토리를 재귀적으로 순회하는 함수를 직접 작성할 수 있지만, filepath 표준 라이브러리에서 이미 유사한 기능의 함수를 제공하므로 바로 사용할 수 있습니다. 간단한 디렉토리 복사 코드 예제는 다음과 같습니다.

go
func CopyDir(src, dst string) error {
    // 원본 디렉토리 상태 확인
  _, err := os.Stat(src)
  if err != nil {
    return err
  }

  return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
    if err != nil {
      return err
    }

        // 상대 경로 계산
    rel, err := filepath.Rel(src, path)
    if err != nil {
      return err
    }

        // 대상 경로拼接
    destpath := filepath.Join(dst, rel)

        // 디렉토리 생성
    var dirpath string
    var mode os.FileMode = 0755
    if info.IsDir() {
      dirpath = destpath
      mode = info.Mode()
    } else if info.Mode().IsRegular() {
      dirpath = filepath.Dir(destpath)
    }

    if err := os.MkdirAll(dirpath, mode); err != nil {
      return err
    }

        // 파일 생성
    if info.Mode().IsRegular() {
      srcfile, err := os.Open(path)
      if err != nil {
        return err
      }
            // 반드시 파일을 닫아야 함
      defer srcfile.Close()
      destfile, err := os.OpenFile(destpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
      if err != nil {
        return err
      }
      defer destfile.Close()

            // 파일 내용 복사
      if _, err := io.Copy(destfile, srcfile); err != nil {
        return err
      }
      return nil
    }

    return nil
  })
}

filepath.Walk 는 디렉토리를 재귀적으로 순회하며, 디렉토리를 만나면 디렉토리를 생성하고 파일을 만나면 새 파일을 생성하여 복사합니다. 코드는 파일 복사에 비해 많지만 복잡하지는 않습니다.

Golang by www.golangdev.cn edit