Menu

Odczyt i zapis plików w Go (Golang): os, bufio i katalogi

Jak czytać i zapisywać pliki w Go: os.ReadFile i os.WriteFile, czytanie linia po linii przez bufio.Scanner, dopisywanie przez os.OpenFile, sprawdzanie, czy plik istnieje, i praca z katalogami.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Odczyt i zapis całego pliku

os.WriteFile i os.ReadFile pokrywają większość potrzeb. W jednym wywołaniu otwierają plik, zapisują lub czytają i go zamykają.

Przykłady na tej stronie działają w katalogu tymczasowym z os.MkdirTemp i usuwają go przez defer os.RemoveAll(dir), więc nic po sobie nie zostawiają. We własnym kodzie ścieżka względna, taka jak "config.json", jest rozwiązywana względem katalogu roboczego procesu, który niekoniecznie jest katalogiem pliku źródłowego ani programu wykonywalnego.

os.WriteFile w razie potrzeby tworzy plik, a jeśli istnieje, obcina go. Trzeci argument to uprawnienia Unix dla nowo tworzonego pliku: 0o644 oznacza, że właściciel może czytać i zapisywać, a wszyscy inni mogą czytać. Dla pliku, który już istnieje, argument jest ignorowany, a umask procesu może usunąć część bitów.

os.ReadFile wczytuje wszystko do pamięci. To dobre rozwiązanie dla plików konfiguracyjnych i małych danych wejściowych, a złe dla wielogigabajtowego logu.

Czytanie linia po linii przez bufio.Scanner

Dla dużych plików, albo gdy i tak chcesz dostać linie, użyj bufio.Scanner. Czyta porcjami i podaje po jednej linii naraz, bez znaku nowej linii.

Trzy szczegóły:

  • Po pętli sprawdź sc.Err(). Scan zwraca false zarówno na końcu pliku, jak i przy błędzie, i tylko Err pozwala je odróżnić.
  • Limit 64 KB na linię. Domyślnie pojedyncza linia dłuższa niż 64 KB zatrzymuje scanner z błędem bufio.Scanner: token too long. Dla plików z długimi liniami (zminifikowany JSON, niektóre logi) podnieś limit przed pętlą: sc.Buffer(make([]byte, 1024*1024), 10*1024*1024).
  • Inne jednostki. sc.Split(bufio.ScanWords) zwraca słowa; bufio.ScanRunes zwraca znaki.

Do czytania strumienia porcjami o stałym rozmiarze zamiast liniami użyj f.Read(buf) w pętli albo io.Copy do innego writera.

Zapis: os.Create, os.OpenFile i dopisywanie

os.Create(name) otwiera plik do zapisu, tworząc go lub obcinając. os.OpenFile daje pełną kontrolę przez flagi:

FlagaZnaczenie
os.O_RDONLY, os.O_WRONLY, os.O_RDWRotwarcie do odczytu, zapisu lub obu (wybierz jedno)
os.O_CREATEutwórz plik, jeśli nie istnieje
os.O_TRUNCwyczyść plik przy otwarciu
os.O_APPENDkażdy zapis trafia na koniec
os.O_EXCLz O_CREATE: błąd, jeśli plik już istnieje

os.Open(name) to OpenFile(name, O_RDONLY, 0). os.Create(name) to OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0o666).

Przy zapisie błąd z Close ma znaczenie. Niektóre systemy plików zgłaszają błędy zapisu dopiero przy zamykaniu, więc samo defer f.Close() może ukryć utracone dane. Dla plików, do których zapisujesz, sprawdzaj Close jawnie, tak jak robi to appendLine. Dla plików, które tylko czytasz, defer f.Close() wystarczy.

fmt.Fprintln i pozostałe funkcje wypisujące z fmt przyjmują dowolny io.Writer, w tym plik. bufio.Writer zbiera małe zapisy w pamięci. Zapomniane w.Flush() to klasyczny błąd: program kończy się normalnie, a ostatnie kilka kilobajtów nigdy nie trafia do pliku.

Czy plik istnieje?

Go nie ma os.Exists. Wywołaj os.Stat i sprawdź błąd:

errors.Is(err, fs.ErrNotExist) to obecny idiom. Zastępuje starsze os.IsNotExist(err), które nie widzi opakowanych błędów.

Sprawdzanie przed otwarciem często jest zbędne i podatne na wyścigi: plik może pojawić się lub zniknąć między sprawdzeniem a otwarciem. Zwykle po prostu otwierasz plik i obsługujesz fs.ErrNotExist z otwarcia. Żeby utworzyć plik tylko wtedy, gdy jeszcze nie istnieje, użyj O_CREATE|O_EXCL, co łączy sprawdzenie i utworzenie w jeden atomowy krok.

Katalogi

ZadanieFunkcja
utworzenie jednego kataloguos.Mkdir(path, 0o755)
utworzenie ścieżki z katalogami nadrzędnymios.MkdirAll(path, 0o755)
wylistowanie kataloguos.ReadDir(path)
przejście po drzewiefilepath.WalkDir(root, fn)
usunięcie pliku lub pustego kataloguos.Remove(path)
usunięcie drzewaos.RemoveAll(path)
zmiana nazwy lub przeniesienieos.Rename(old, new)
plik lub katalog tymczasowyos.CreateTemp("", "prefix-*"), os.MkdirTemp("", "prefix")
łączenie części ścieżkifilepath.Join(a, b, c)

Do ścieżek w systemie plików używaj path/filepath: stosuje właściwy separator dla systemu operacyjnego (\ w Windows). Pakiet path jest do ścieżek rozdzielanych ukośnikiem, takich jak URL-e.

Go 1.24 dodało też os.Root (os.OpenRoot(dir)), które otwiera pliki tylko w obrębie jednego katalogu i odrzuca ścieżki, które z niego uciekają przez .. lub dowiązania symboliczne. Używaj go, gdy nazwy plików pochodzą od użytkowników.

Typowe błędy

  • Brak sprawdzania błędów. Każde z tych wywołań może się nie powieść. Plik, którego nie udało się otworzyć, to nil, a każde późniejsze Read, Write czy Close na nim zwraca invalid argument, co ukrywa prawdziwą przyczynę (pliku nie było albo zabrakło uprawnień).
  • Zapomniane sc.Err() po pętli skanującej. Błąd odczytu wygląda jak koniec pliku.
  • Zapomniane Flush na bufio.Writer. Brakuje końca pliku.
  • Ignorowanie błędu z Close po zapisie. Błędy zapisu mogą ujawnić się tylko tam.
  • defer f.Close() w pętli po wielu plikach. Pliki pozostają otwarte, dopóki funkcja nie zwróci wyniku, i mogą skończyć się deskryptory plików. Przenieś ciało pętli do funkcji, żeby każdy plik zamykał się w swojej iteracji.
  • Uprawnienia zapisane dziesiętnie. 644 to nie 0o644. Go czyta 644 jako liczbę dziesiętną, czyli 0o1204, co ustawia dziwne bity.

Najczęściej zadawane pytania

Jak wczytać cały plik do stringa w Go?

data, err := os.ReadFile("notes.txt") zwraca zawartość jako []byte; skonwertuj ją przez string(data). Funkcja sama otwiera, czyta i zamyka plik. Używaj jej dla plików, które swobodnie mieszczą się w pamięci; duże pliki czytaj linia po linii przez bufio.Scanner.

Jak czytać plik linia po linii w Go?

Otwórz plik przez os.Open, dodaj defer f.Close(), opakuj go w bufio.NewScanner(f), iteruj pętlą for sc.Scan() { line := sc.Text() } i po pętli sprawdź sc.Err(). Linie dłuższe niż 64 KB sprawiają, że scanner kończy się błędem token too long, chyba że powiększysz jego bufor przez sc.Buffer.

Jak dopisać dane do pliku w Go?

Otwórz go przez os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644), zapisz dane i sprawdź błąd z Close. O_CREATE tworzy plik, jeśli nie istnieje, a O_APPEND sprawia, że każdy zapis trafia na koniec.

Jak sprawdzić, czy plik istnieje w Go?

Wywołaj os.Stat(path) i sprawdź błąd przez errors.Is(err, fs.ErrNotExist). Błąd nil oznacza, że plik istnieje. Każdy inny błąd (na przykład brak uprawnień) oznacza, że nie da się tego stwierdzić, więc obsłuż go osobno, zamiast traktować jak "nie istnieje".

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ