파일
Go 언어의 파일 처리 표준 라이브러리는 대략 다음과 같습니다:
os라이브러리, OS 파일 시스템 상호작용의 구체적인 구현을 담당io라이브러리, IO 읽기/쓰기의 추상화 계층fs라이브러리, 파일 시스템의 추상화 계층
본 문서에서는 Go 언어를 통한 기본적인 파일 처리 방법을 설명합니다.
열기
파일을 여는 일반적인 두 가지 방법은 os 패키지에서 제공하는 두 가지 함수를 사용하는 것입니다. Open 함수는 파일 포인터와 에러를 반환합니다.
func Open(name string) (*File, error)후자의 OpenFile 은 더욱 세밀한 제어를 제공할 수 있으며, 함수 Open 은 OpenFile 함수를 간단히 래핑한 것입니다.
func OpenFile(name string, flag int, perm FileMode) (*File, error)먼저 첫 번째 사용 방법을 소개하겠습니다. 파일 이름만 제공하면 됩니다. 코드는 다음과 같습니다.
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 패키지도 이를 위한 편의 함수를 제공합니다. 수정된 코드는 다음과 같습니다.
func main() {
file, err := os.Open("README.txt")
if os.IsNotExist(err) {
fmt.Println("파일이 존재하지 않습니다")
} else if err != nil {
fmt.Println("파일 접근 오류")
} else {
fmt.Println("파일 읽기 성공", file)
}
}다시 실행하면 출력은 다음과 같습니다.
파일이 존재하지 않습니다실제로 첫 번째 함수로 읽은 파일은 읽기 전용일 뿐이며 수정할 수 없습니다.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}OpenFile 함수를 사용하면 파일 디스크립터와 파일 권한 등 더 많은 세부 사항을 제어할 수 있습니다. 파일 디스크립터에 대해 os 패키지는 다음과 같은 상수를 제공합니다.
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 // 열기 시 쓰기 가능한 파일 잘라내기
)파일 권한에 대해서는 다음과 같은 상수를 제공합니다.
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 으로 모든 사람이 파일에 읽기/쓰기를 할 수 있으며, 없을 경우 자동으로 생성됩니다.
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() 함수를 사용할 수 있습니다. 코드 예제는 다음과 같습니다.
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 문에 배치합니다.
defer file.Close()읽기
파일을 성공적으로 연 후에는 읽기 작업을 수행할 수 있습니다. 파일 읽기 작업에 대해 *os.File 타입은 다음과 같은 공개 메서드를 제공합니다.
// 파일을传入된 바이트 슬라이스에 읽기
func (f *File) Read(b []byte) (n int, err error)
// 첫 번째 방법에 비해 지정된 오프셋에서 읽을 수 있음
func (f *File) ReadAt(b []byte, off int64) (n int, err error)대부분의 경우 첫 번째 방법을 더 많이 사용합니다. 첫 번째 방법의 경우 읽기 시 슬라이스의 동적 확장을 위한 로직을 직접 작성해야 합니다. 코드는 다음과 같습니다.
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
}
}
}나머지 로직은 다음과 같습니다.
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
func ReadFile(name string) ([]byte, error)사용 예제는 다음과 같습니다.
func main() {
bytes, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
}출력은 다음과 같습니다.
hello world!io.ReadAll
func ReadAll(r Reader) ([]byte, error)사용 예제는 다음과 같습니다.
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 구조체는 데이터 쓰기를 위해 다음과 같은 메서드를 제공합니다.
// 바이트 슬라이스 쓰기
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 으로 지정하여 데이터를 쓰는 예제입니다.
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 모드로 파일을 열었으므로 파일 작성 시 데이터가 파일 끝에 추가됩니다. 실행 완료 후 파일 내용은 다음과 같습니다.
hello world!
hello world!
hello world!
hello world!
hello world!파일에 바이트 슬라이스를 쓰는 것도 유사한 작업이므로 더 이상 설명하지 않습니다. 쓰기 작업에 대해 표준 라이브러리는 편의 함수를 제공합니다. 각각 os.WriteFile 와 io.WriteString 입니다.
os.WriteFile
func WriteFile(name string, data []byte, perm FileMode) error사용 예제는 다음과 같습니다.
func main() {
err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
if err != nil {
fmt.Println(err)
}
}이때 파일 내용은 다음과 같습니다.
hello world!io.WriteString
func WriteString(w Writer, s string) (n int, err error)사용 예제는 다음과 같습니다.
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 을 래핑한 것입니다.
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}WARNING
파일을 생성할 때 부모 디렉토리가 존재하지 않으면 생성에 실패하고 에러가 반환됩니다.
복사
파일을 복사하려면 두 개의 파일을 동시에 열어야 합니다. 첫 번째 방법은 원본 파일의 데이터를 읽은 후 대상 파일에 쓰는 것입니다. 코드 예제는 다음과 같습니다.
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 메서드를 사용하는 것입니다. 파일을 열 때 하나는 읽기 전용, 다른 하나는 쓰기 전용으로 엽니다.
func (f *File) ReadFrom(r io.Reader) (n int64, err error)사용 예제는 다음과 같습니다.
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 입니다.
func Copy(dst Writer, src Reader) (written int64, err error)사용 예제는 다음과 같습니다.
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 함수를 사용합니다.
func Rename(oldpath, newpath string) error예제는 다음과 같습니다.
func main() {
err := os.Rename("README.txt", "readme.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("이름 바꾸기 성공")
}
}이 함수는 디렉토리에도 동일하게 적용됩니다.
삭제
삭제 작업은 다른 작업에 비해 간단하며, os 패키지의 두 가지 함수만 사용합니다.
// 단일 파일 또는 빈 디렉토리 삭제, 디렉토리가 비어 있지 않으면 에러 반환
func Remove(name string) error
// 지정된 디렉토리의 모든 파일과 디렉토리 (하위 디렉토리와 하위 파일 포함) 삭제
func RemoveAll(path string) error사용법은 매우 간단합니다. 다음은 디렉토리를 삭제하는 예제입니다.
func main() {
// 현재 디렉토리의 모든 파일과 하위 디렉토리 삭제
err := os.RemoveAll(".")
if err != nil {
fmt.Println(err)
}else {
fmt.Println("삭제 성공")
}
}다음은 단일 파일을 삭제하는 예제입니다.
func main() {
// 현재 디렉토리의 파일 삭제
err := os.Remove("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("삭제 성공")
}
}플러시
os.Sync 는 하위 시스템 호출 Fsync 을 래핑한 함수로, 운영체제의 캐시된 IO 를 디스크에 기록하는 데 사용됩니다.
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 함수를 사용하는 것입니다.
func ReadDir(name string) ([]DirEntry, error)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 을 간단히 래핑한 것입니다.
// n < 0 일 때 디렉토리의 모든 내용 읽기
func (f *File) ReadDir(n int) ([]DirEntry, error)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 패키지의 두 가지 함수를 사용합니다.
// 지정된 권한으로 지정된 이름의 디렉토리 생성
func Mkdir(name string, perm FileMode) error
// 전자에 비해 이 함수는 모든 필요한 부모 디렉토리를 생성
func MkdirAll(path string, perm FileMode) error예제는 다음과 같습니다.
func main() {
err := os.Mkdir("src", 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("생성 성공")
}
}복사
디렉토리를 재귀적으로 순회하는 함수를 직접 작성할 수 있지만, filepath 표준 라이브러리에서 이미 유사한 기능의 함수를 제공하므로 바로 사용할 수 있습니다. 간단한 디렉토리 복사 코드 예제는 다음과 같습니다.
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 는 디렉토리를 재귀적으로 순회하며, 디렉토리를 만나면 디렉토리를 생성하고 파일을 만나면 새 파일을 생성하여 복사합니다. 코드는 파일 복사에 비해 많지만 복잡하지는 않습니다.
