Go'da küçük bir dosyayı tek seferde okumak için os.ReadFile, yazmak için os.WriteFile yeterlidir. Büyük dosyaları satır satır işlemek için dosyayı os.Open ile açıp bufio.Scanner ile okursunuz; çok sayıda küçük yazım yapacaksanız os.Create ile açtığınız dosyayı bufio.Writer ile sarar ve sonunda Flush çağırırsınız. Bütün bu yapıların ortak paydası io.Reader ve io.Writer arayüzleridir.

Bu yazı os, io ve bufio paketlerinin dosya işlemlerinde nasıl birlikte çalıştığını, hataların nasıl kontrol edileceğini, ekleme kipini (append), io.EOF anlamını ve Go 1.24 ile gelen os.Root türünün dizin dışına çıkan yolları nasıl engellediğini Go 1.27.3 ile çalıştırılmış örneklerle anlatıyor. Örnekler geçici bir dizinde çalışır ve sonunda bu dizini siler. Konunun öğrenme yolundaki yeri için Go öğrenme rehberine bakabilirsiniz.

Tek seferde okuma ve yazma: os.ReadFile ve os.WriteFile

os.ReadFile(ad) dosyanın tamamını okuyup []byte olarak döndürür. Belge, başarılı bir çağrının err == nil döndürdüğünü, io.EOF döndürmediğini vurgular: fonksiyon zaten dosyanın sonuna kadar okumak için tasarlandığından dosya sonunu hata saymaz. os.WriteFile(ad, veri, izin) ise dosya yoksa verilen izinlerle (umask uygulanmadan önceki haliyle) oluşturur, varsa izinleri değiştirmeden içeriğini sıfırlayıp yeniden yazar. Belge ayrıca bu işlemin birden fazla sistem çağrısı gerektirdiğini, yarıda kalan bir hata durumunda dosyanın kısmen yazılmış kalabileceğini not eder.

go
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"log"
	"os"
	"path/filepath"
)

func main() {
	dir, err := os.MkdirTemp("", "ornek")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)

	yol := filepath.Join(dir, "not.txt")
	if err := os.WriteFile(yol, []byte("Merhaba, Gopher!\n"), 0644); err != nil {
		log.Fatal(err)
	}

	veri, err := os.ReadFile(yol)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d bayt: %q\n", len(veri), veri)

	_, err = os.ReadFile(filepath.Join(dir, "yok.txt"))
	fmt.Println(errors.Is(err, fs.ErrNotExist))

	var pe *fs.PathError
	if errors.As(err, &pe) {
		fmt.Println("işlem:", pe.Op, "| dosya:", filepath.Base(pe.Path))
	}
}

// Çıktı:
// 17 bayt: "Merhaba, Gopher!\n"
// true
// işlem: open | dosya: yok.txt

Var olmayan bir dosyayı okumaya çalışınca dönen hatanın metni işletim sistemine göre değişir; Linux'ta "no such file or directory", Windows'ta sistemin kendi iletisi görünür. Bu yüzden hatanın metnini karşılaştırmak yerine errors.Is(err, fs.ErrNotExist) kullanın. Belge, eski os.IsNotExist fonksiyonunun errors.Is öncesinden kaldığını ve yeni kodda errors.Is(err, fs.ErrNotExist) tercih edilmesini söyler. Dosya adı alan os fonksiyonlarının hataları *fs.PathError tipindedir; errors.As ile işlem adını (Op) ve yolu (Path) ayrı ayrı alabilirsiniz. Hata zincirleri için hata yönetimi yazısına bakın.

Dosya açma: Open, Create ve OpenFile

Dosya açma: Open, Create ve OpenFile: os.Open(ad), os.Create(ad), os.OpenFile(ad, bayrak, izin)

Dosyayla birden fazla adımda çalışacaksanız onu açıp bir *os.File değeri alırsınız. Üç temel fonksiyon şunlardır:

FonksiyonKipDosya yoksaDosya varsa
os.Open(ad)yalnızca okuma (O_RDONLY)hata dönerokumak için açar
os.Create(ad)okuma ve yazma (O_RDWR)0666 izinle oluştururiçeriği sıfırlar
os.OpenFile(ad, bayrak, izin)bayraklarla seçilirO_CREATE varsa oluştururbayraklara göre davranır

OpenFile genel çağrıdır ve os.O_RDONLY, os.O_WRONLY ya da os.O_RDWR bayraklarından tam olarak birini gerektirir. Bunlara | ile O_APPEND (sona ekle), O_CREATE (yoksa oluştur), O_EXCL (O_CREATE ile birlikte, dosya önceden var olmamalı), O_TRUNC (açarken içeriği sil) ve O_SYNC eklenebilir. Belge her bayrağın her sistemde uygulanmayabileceğini de belirtir. Her durumda dosyanın bulunacağı dizin önceden var olmalıdır; gerekiyorsa os.MkdirAll ile oluşturursunuz.

Log dosyası gibi sona ekleme yapılan dosyalarda yaygın kalıp aşağıdadır. Kalıp, os belgesindeki resmî ekleme örneğini izler: yazma hatası alındığında dosya yine kapatılır ama bildirilen hata yazma hatasıdır; yazma başarılıysa Close da bir error döndürdüğü için onun sonucu da kontrol edilir:

go
package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
)

func ekle(yol, satir string) error {
	f, err := os.OpenFile(yol, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	if _, err := f.WriteString(satir + "\n"); err != nil {
		f.Close()
		return err
	}
	return f.Close()
}

func main() {
	dir, err := os.MkdirTemp("", "ornek")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)
	yol := filepath.Join(dir, "erisim.log")

	for _, s := range []string{"giriş", "sorgu", "çıkış"} {
		if err := ekle(yol, s); err != nil {
			log.Fatal(err)
		}
	}
	veri, _ := os.ReadFile(yol)
	fmt.Print(string(veri))

	bilgi, err := os.Stat(yol)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(bilgi.Name(), bilgi.Size(), bilgi.IsDir())
}

// Çıktı:
// giriş
// sorgu
// çıkış
// erisim.log 23 false

Dosya boyutu 23 bayttır, karakter sayısı değil: "ş", "ç" ve "ı" UTF-8'de ikişer bayt tutar. os.Stat bir FileInfo döndürür; Name, Size, IsDir, Mode ve ModTime metotlarıyla dosya hakkında bilgi alırsınız. Belgeye göre *os.File metotları eşzamanlı kullanım için güvenlidir, ama aynı dosyaya birden çok goroutine'den yazarken satırların sırasını uygulamanızın kendisi düzenlemelidir.

Dizinler için de benzer kısa fonksiyonlar vardır. os.MkdirAll(yol, 0755) gereken tüm üst dizinlerle birlikte bir dizin oluşturur; dizin zaten varsa hiçbir şey yapmadan nil döndürür, bu yüzden programın başında güvenle çağrılabilir. os.ReadDir(yol) dizindeki girdileri dosya adına göre sıralanmış olarak verir; belgeye göre okuma sırasında hata olursa o ana kadar okuyabildiği girdileri hatayla birlikte döndürür. os.Remove tek bir dosyayı veya boş dizini, os.RemoveAll ise bir dizini içindekilerle birlikte siler. Örneklerimizde os.MkdirTemp ile geçici dizin açıp defer os.RemoveAll(dir) ile temizlememiz bu yüzdendir.

Satır satır okuma: bufio.Scanner ve bufio.Writer

bufio.Scanner, bir io.Reader üzerinden art arda "jeton" okumanın en kolay yoludur. Varsayılan bölme fonksiyonu ScanLines'dır: her satırı satır sonu işareti çıkarılmış olarak verir. Belgeye göre satır sonu bir isteğe bağlı \r ve ardından gelen zorunlu \n karakteridir; yani Windows tarzı \r\n ile biten satırlar da temiz gelir. Sondaki boş olmayan satır, satır sonu olmasa bile döndürülür; boş satırlar da boş string olarak gelir. Scan false döndürdüğünde döngü biter ve okuma sırasında bir hata olduysa Err onu verir; dosya sonu (io.EOF) hata sayılmaz.

Yazma tarafında bufio.Writer verileri bellekte biriktirir ve tampon dolunca alttaki yazıcıya gönderir. Çok sayıda küçük yazımı tek sistem çağrısında topladığı için hızlıdır, ama sonunda Flush çağırmazsanız tampondaki son veriler dosyaya hiç ulaşmaz. Belge, bir yazma hatasından sonra yazıcının yeni veri kabul etmediğini ve sonraki tüm yazımların ve Flush çağrısının aynı hatayı döndürdüğünü belirtir.

go
package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"path/filepath"
)

func yaz(yol string) error {
	f, err := os.Create(yol)
	if err != nil {
		return err
	}
	w := bufio.NewWriter(f)
	for i, sehir := range []string{"Ankara", "İzmir", "", "Muğla"} {
		fmt.Fprintf(w, "%d;%s\r\n", i+1, sehir)
	}
	if err := w.Flush(); err != nil {
		f.Close()
		return err
	}
	return f.Close()
}

func oku(yol string) error {
	f, err := os.Open(yol)
	if err != nil {
		return err
	}
	defer f.Close()

	sc := bufio.NewScanner(f)
	satir := 0
	for sc.Scan() {
		satir++
		fmt.Printf("%d: %q\n", satir, sc.Text())
	}
	return sc.Err()
}

func main() {
	dir, err := os.MkdirTemp("", "ornek")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)
	yol := filepath.Join(dir, "sehirler.txt")

	if err := yaz(yol); err != nil {
		log.Fatal(err)
	}
	if err := oku(yol); err != nil {
		log.Fatal(err)
	}
}

// Çıktı:
// 1: "1;Ankara"
// 2: "2;İzmir"
// 3: "3;"
// 4: "4;Muğla"

Dosyayı \r\n ile yazdığımız halde %q çıktısında \r görünmüyor; ScanLines onu kırptı. Okuma tarafında defer f.Close() kullanmak yeterlidir, çünkü yalnızca okunan bir dosyada kapatma hatası pratikte bilgi taşımaz. Yazma tarafında ise Close sonucunu döndürdük. defer deyiminin sırası ve davranışı için defer, panic ve recover yazısına bakabilirsiniz. Ayrıştırılmış alanları sayıya çevirmek için strconv kullanılır; gerçek CSV dosyaları için tırnak ve kaçış kurallarını bilen encoding/csv paketi daha doğru bir seçimdir, biçimin kendisi CSV formatı yazısında anlatılıyor.

io.Reader ve io.Writer: her şeyin ortak dili

io.Reader tek bir metodu olan bir arayüzdür: Read(p []byte) (n int, err error). io.Writer da aynı şekilde Write metodunu tanımlar. *os.File, strings.Reader, bytes.Buffer, ağ bağlantıları ve HTTP gövdeleri bu arayüzleri uygular; bu yüzden io.Copy, io.ReadAll, bufio.Scanner ve fmt.Fprintf hepsiyle aynı şekilde çalışır. Arayüzlerin örtük uygulanması interface yazısında anlatılıyor.

Read sözleşmesinin bir inceliği var: belgeye göre bir okuma hem n > 0 bayt hem de hata döndürebilir. Çağıran tarafın önce gelen n baytı işleyip sonra hataya bakması gerekir. Girdi bittiğinde Read, io.EOF döndürür; bu bir hata değil, verinin bittiğini bildiren bir sinyaldir. Belge Read uygulamalarının io.EOF'u sarmalamadan kendisi olarak döndürmesi gerektiğini, çünkü çağıranların == ile kontrol ettiğini de belirtir. Kendi yazıcınızı yazmak için tek bir metot yeterlidir:

go
package main

import (
	"fmt"
	"io"
	"log"
	"os"
	"strings"
)

// sayac, kendisine yazılan bayt sayısını tutan bir io.Writer.
type sayac struct{ n int }

func (s *sayac) Write(p []byte) (int, error) {
	s.n += len(p)
	return len(p), nil
}

func main() {
	r := strings.NewReader("Go'da her şey Reader ve Writer.\n")
	n, err := io.Copy(os.Stdout, r)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("kopyalanan:", n)

	ilk, err := io.ReadAll(io.LimitReader(strings.NewReader("0123456789"), 4))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", ilk)

	var s sayac
	w := io.MultiWriter(os.Stdout, &s)
	fmt.Fprintln(w, "çift kayıt")
	fmt.Println("sayaç:", s.n)

	buf := make([]byte, 4)
	r2 := strings.NewReader("abcdefghij")
	var parcalar []string
	for {
		n, err := r2.Read(buf)
		if n > 0 {
			parcalar = append(parcalar, fmt.Sprintf("%q", buf[:n]))
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
	}
	fmt.Println(strings.Join(parcalar, " "))
}

// Çıktı:
// Go'da her şey Reader ve Writer.
// kopyalanan: 33
// 0123
// çift kayıt
// sayaç: 13
// "abcd" "efgh" "ij"

io.Copy kaynaktan io.EOF gelene kadar okur ve başarı durumunda nil döndürür; kopyalanan miktar bayt cinsinden 33'tür. io.LimitReader bir okuyucuyu belirli bir bayt sayısıyla sınırlar; dışarıdan gelen veriyi belleğe alırken üst sınır koymanın basit yoludur. io.MultiWriter her yazımı birden fazla hedefe kopyalar. Son döngü Read sözleşmesinin elle uygulanmış halidir: önce gelen baytlar işlenir, sonra hata kontrol edilir. Dosyayı bellek dostu biçimde başka bir yere aktarmak için io.Copy(hedefDosya, kaynakDosya) çoğu zaman tüm içeriği ReadFile ile okumaktan daha iyidir. Go 1.26 sürüm notları, io.ReadAll fonksiyonunun artık daha az ara bellek ayırdığını ve sonuç dilimini en küçük boyutta döndürdüğünü belirtir.

os.Root ile dizin dışına çıkmayı engellemek

Kullanıcının verdiği bir dosya adını bir dizinle birleştirip açmak, ../../ içeren adlar yüzünden tehlikelidir. Go 1.24 sürüm notları bu sorun için os.Root türünü getirdi: os.OpenRoot(dizin) bir dizini açar ve dönen değerin metotları yalnızca bu dizinin altında çalışır. Belgeye göre bir dosya adının herhangi bir parçası kökün dışına çıkıyorsa metot hata döner; sembolik bağlantılar izlenir ama kökün dışını gösteremez ve mutlak olamaz. Go 1.25 sürüm notları türe MkdirAll, ReadFile, WriteFile, RemoveAll ve Rename gibi yeni metotlar ekledi.

go
package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	dir, err := os.MkdirTemp("", "kok")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)

	root, err := os.OpenRoot(dir)
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()

	if err := root.MkdirAll("yuklemeler/2026", 0755); err != nil {
		log.Fatal(err)
	}
	if err := root.WriteFile("yuklemeler/2026/a.txt", []byte("güvenli"), 0644); err != nil {
		log.Fatal(err)
	}
	veri, err := root.ReadFile("yuklemeler/2026/a.txt")
	fmt.Println(string(veri), err)

	_, err = root.ReadFile("../../etc/passwd")
	fmt.Println(err)
}

// Çıktı:
// güvenli <nil>
// openat ../../etc/passwd: path escapes from parent

Belge os.Root'un sınırlarını da açıkça yazar: Linux bind mount'larını, /proc özel dosyalarını ya da Unix aygıt dosyalarına erişimi engellemez; GOOS=js gibi bazı platformlarda kaçışları garanti edemez. os.DirFS ise güvenlik sınırı değildir; belge, sembolik bağlantılar yoluyla kaçışı önleyen bir fs.FS için Root.FS kullanılmasını önerir. Dosyaları programın içine gömüp io/fs üzerinden sunmak isterseniz embed yazısına bakın.

Sık yapılan hatalar

Sık yapılan hatalar: bufio.Writer kullanıp Flush çağırmamak. Program hatasız biter ama dosyanın sonu..., Yazılan dosyada Close hatasını yok saymak. defer f.Close() okuma için...
  • bufio.Writer kullanıp Flush çağırmamak. Program hatasız biter ama dosyanın sonu eksik kalır.
  • Yazılan dosyada Close hatasını yok saymak. defer f.Close() okuma için uygundur; yazmada sonucu kontrol edin.
  • Read döngüsünde önce hataya bakıp n baytı atlamak. Son parça ile io.EOF aynı çağrıda gelebilir.
  • Hata metnini strings.Contains(err.Error(), "no such file") ile kontrol etmek. Metin işletim sistemine göre değişir; errors.Is(err, fs.ErrNotExist) kullanın.
  • Döngü içinde açılan dosyaları defer ile kapatmak. Ertelenen çağrılar fonksiyon bitince çalışır; binlerce dosyada açık tanıtıcı sınırına takılabilirsiniz. Her dosyayı ayrı bir fonksiyonda işleyin.
  • Kullanıcıdan gelen dosya adını filepath.Join ile birleştirip güvenli sanmak. Join yolu temizler ama .. ile üst dizine çıkışı engellemez; bunun için os.Root var.
  • Çok büyük bir dosyayı os.ReadFile ile belleğe almak. Akış olarak işlenebilen veride bufio.Scanner veya io.Copy kullanın.

Sık Sorulan Sorular

Go'da bir dosya satır satır nasıl okunur?

Dosyayı os.Open ile açın, bufio.NewScanner(f) ile bir tarayıcı oluşturun, for sc.Scan() döngüsünde sc.Text() ile satırı alın ve döngüden sonra sc.Err() sonucunu kontrol edin. Satırlar satır sonu karakteri olmadan gelir.

Dosyanın var olup olmadığını nasıl anlarım?

_, err := os.Stat(yol) çağırıp errors.Is(err, fs.ErrNotExist) ile kontrol edebilirsiniz. Ancak "önce kontrol et, sonra aç" yaklaşımı iki adım arasında dosya değişebileceği için yarış durumuna açıktır. Çoğu durumda doğrudan açmayı deneyip hatayı işlemek daha sağlamdır; yalnızca yeni dosya oluşturmak istiyorsanız O_CREATE|O_EXCL bayrakları dosya zaten varsa hata verir.

0644 ve 0755 izinleri ne anlama gelir?

Bunlar Unix tarzı sekizlik izin bitleridir: 0644 sahibine okuma ve yazma, diğerlerine yalnızca okuma verir; 0755 buna çalıştırma bitlerini ekler ve dizinler için yaygındır. Belgeye göre bu değerler umask uygulanmadan önceki izinlerdir. Windows'ta durum farklıdır: os.Chmod belgesine göre orada yalnızca sahibin yazma biti (0o200) kullanılır ve dosyanın salt okunur özniteliğini belirler. Linux izinleri için Linux nedir yazısına bakabilirsiniz.

ioutil.ReadFile hâlâ kullanılmalı mı?

Hayır. io/ioutil paketi belgesinde Go 1.16'dan beri kullanımdan kaldırılmış (deprecated) olarak işaretlidir; aynı işlevler artık io ve os paketlerinde bulunur ve yeni kodda onlar tercih edilmelidir. Örneğin ioutil.ReadFile yalnızca os.ReadFile fonksiyonunu çağırır. Eski kodda gördüğünüz çağrıları os.ReadFile, os.WriteFile, io.ReadAll ve io.Discard ile değiştirebilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Go öğrenme rehberi · fmt ile biçimlendirme · encoding/json kullanımı · go:embed ile dosya gömme · defer, panic ve recover · Go hata yönetimi