go
Go dersleri
Go/Ekosistem ve Pratik

Dosyalar, I/O ve JSON

io.Reader/Writer, os, bufio ve encoding/json ile veri işleme.

Ders 20 / 2125 dkOrta
Bu derste öğreneceklerin
  • io.Reader ve io.Writer soyutlaması
  • os.ReadFile, os.WriteFile ve os.Open
  • bufio.Scanner ile satır satır okuma
  • defer ile kaynakları kapatma
  • encoding/json: Marshal ve Unmarshal
  • Struct etiketleri: json:"ad,omitempty"
  • json.Decoder ile akış işleme

Bir program ne kadar iyi yazılmış olursa olsun, dış dünyayla konuşmuyorsa pek bir işe yaramaz. Veri bir yerden gelir — bir dosyadan, bir ağ bağlantısından, standart girdiden — işlenir ve bir yere gider. Go bu işi, standart kütüphanenin en zarif tasarımlarından biriyle çözer: io.Reader ve io.Writer.

Bu iki arayüz yalnızca birer metot içerir, ama Go'nun tüm giriş/çıkış dünyası onların üzerine kuruludur. Bir dosya, bir ağ bağlantısı, bellekteki bir tampon, sıkıştırılmış bir akış, şifrelenmiş bir kanal — hepsi aynı iki arayüzü uygular. Sonuç olarak, dosyadan okuyan bir fonksiyon hiçbir değişiklik yapılmadan ağdan da okuyabilir. Bu, "küçük arayüzler, büyük esneklik" fikrinin en güzel örneğidir.

Bu derste io.Reader ve io.Writer soyutlamasını, dosya okuma ve yazmanın farklı yollarını, büyük dosyaları satır satır işlemeyi, kaynakları güvenle kapatmayı, JSON'a dönüştürme ve JSON'dan okuma işlemlerini, struct etiketlerinin ayrıntılarını ve akış hâlindeki JSON verisini işlemeyi öğreneceksin.

io.Reader ve io.Writer

İki arayüzün tanımı şaşırtıcı derecede küçüktür:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

Read, verilen tamponu doldurmaya çalışır ve kaç bayt yazdığını söyler. Akış bittiğinde io.EOF hatasını döndürür. Write ise verilen baytları yazar ve kaç bayt yazabildiğini bildirir.

Bu kadar küçük olmalarının gücü şudur: Fonksiyonun *os.File değil de io.Reader alması, onu test edilebilir ve yeniden kullanılabilir kılar:

main.go
package main

import (
	"bytes"
	"fmt"
	"io"
	"strings"
)

// countWords herhangi bir kaynaktan okuyabilir: dosya, ağ, bellek…
func countWords(r io.Reader) (int, error) {
	data, err := io.ReadAll(r)
	if err != nil {
		return 0, err
	}
	return len(strings.Fields(string(data))), nil
}

func main() {
	// 1) Bellekteki metin
	n, _ := countWords(strings.NewReader("go hızlı ve basit bir dildir"))
	fmt.Println("kelime sayısı:", n)

	// 2) Bir tampon
	var buf bytes.Buffer
	buf.WriteString("ikinci kaynak da aynı fonksiyonla okunur")
	n2, _ := countWords(&buf)
	fmt.Println("kelime sayısı:", n2)

	// io.Copy: bir Reader'dan bir Writer'a aktarır
	var out bytes.Buffer
	written, _ := io.Copy(&out, strings.NewReader("kopyalanan veri"))
	fmt.Printf("%d bayt kopyalandı: %q\n", written, out.String())

	// MultiWriter: aynı veriyi birden çok hedefe yazar
	var a, b bytes.Buffer
	multi := io.MultiWriter(&a, &b)
	fmt.Fprintf(multi, "her iki tampona da yazıldı")
	fmt.Println(a.String() == b.String(), a.Len())
}
Çıktı
kelime sayısı: 6
kelime sayısı: 6
15 bayt kopyalandı: "kopyalanan veri"
true 28

Standart kütüphanede bu arayüzleri uygulayan onlarca tip vardır: os.File, net.Conn, strings.Reader, bytes.Buffer, gzip.Reader, http.Request.Body ve daha fazlası. Kendi fonksiyonlarını yazarken de somut tip yerine bu arayüzleri parametre almayı alışkanlık hâline getir.

Dosya okuma ve yazma

Go dosya işlemleri için üç seviye sunar; hangisini seçeceğin dosyanın boyutuna ve ihtiyacına bağlıdır.

Tek seferde okuma/yazma en basitidir; dosya belleğe sığıyorsa ideal:

main.go
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	const path = "notlar.txt"

	content := "birinci satır\nikinci satır\nüçüncü satır\n"

	// 0644: sahibi okuyup yazabilir, diğerleri okuyabilir
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		fmt.Println("yazma hatası:", err)
		return
	}
	defer os.Remove(path) // örnek bittiğinde temizle

	data, err := os.ReadFile(path)
	if err != nil {
		fmt.Println("okuma hatası:", err)
		return
	}

	fmt.Println("bayt sayısı:", len(data))
	lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
	fmt.Println("satır sayısı:", len(lines))
	fmt.Println("ilk satır:", lines[0])

	// Var olmayan dosya: hata türünü ayırt et
	_, err = os.ReadFile("olmayan-dosya.txt")
	fmt.Println("dosya yok mu:", os.IsNotExist(err))
}
Çıktı
bayt sayısı: 47
satır sayısı: 3
ilk satır: birinci satır
dosya yok mu: true

Dosyayı açıp akış olarak kullanmak, büyük dosyalar için gereklidir. os.Open salt okunur açar, os.Create oluşturur veya içeriğini siler, os.OpenFile ise tüm bayrakları sana bırakır:

f, err := os.Open("büyük.log")
if err != nil {
	return err
}
defer f.Close() // her yolda kapanır

// Ekleme modunda açmak:
g, err := os.OpenFile("günlük.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)

Tamponlanmış okuma, satır satır işlemek için doğru araçtır. bufio.Scanner dosyanın tamamını belleğe almadan, satır satır ilerler:

main.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	const path = "kayitlar.txt"
	content := `2026-01-05 INFO  sunucu başladı
2026-01-05 WARN  yavaş sorgu: 1200ms
2026-01-05 ERROR bağlantı reddedildi
2026-01-06 INFO  yeniden bağlanıldı
2026-01-06 ERROR zaman aşımı
`
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		fmt.Println(err)
		return
	}
	defer os.Remove(path)

	f, err := os.Open(path)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()

	counts := map[string]int{}
	lineNo := 0

	scanner := bufio.NewScanner(f)
	for scanner.Scan() { // satır satır: bellek kullanımı sabit
		lineNo++
		fields := strings.Fields(scanner.Text())
		if len(fields) < 2 {
			continue
		}
		counts[fields[1]]++
	}
	if err := scanner.Err(); err != nil { // taramadaki hatayı kontrol et
		fmt.Println("tarama hatası:", err)
		return
	}

	fmt.Println("toplam satır:", lineNo)
	for _, level := range []string{"INFO", "WARN", "ERROR"} {
		fmt.Printf("%-6s %d\n", level, counts[level])
	}
}
Çıktı
toplam satır: 5
INFO   2
WARN   1
ERROR  2

scanner.Err() kontrolünü atlamak yaygın bir hatadır: Scan() hem dosya bittiğinde hem de bir okuma hatası oluştuğunda false döndürür. İkisini ayırt etmenin tek yolu Err() çağırmaktır.

bufio.Scanner'ın varsayılan satır sınırı 64 KB'tır. Daha uzun satırlar bekliyorsan scanner.Buffer(...) ile tamponu büyütmelisin; aksi hâlde bufio.Scanner: token too long hatası alırsın.

Kaynakları kapatmak

Açtığın her dosya, bağlantı ve akış kapatılmalıdır. Go'da bunun standart biçimi defer'dir ve hemen açma işleminin ardından yazılır:

f, err := os.Open(path)
if err != nil {
	return err       // açılamadıysa kapatılacak bir şey de yok
}
defer f.Close()      // bundan sonra hangi yoldan çıkarsan çık, kapanır

Yazma yaparken bir incelik vardır: Close da hata döndürebilir. İşletim sistemi verileri tamponlar ve gerçek yazma Close sırasında gerçekleşebilir; bu hatayı yutarsan veri kaybını fark etmezsin. Kritik yazmalarda isimlendirilmiş dönüş değeriyle Close hatasını da yakala:

func save(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr // yazma başarılıysa ama kapatma başarısızsa bildir
		}
	}()

	_, err = f.Write(data)
	return err
}

Okuma yaparken bu ayrıntı önemsizdir; defer f.Close() yeterlidir.

JSON: Marshal ve Unmarshal

encoding/json paketi, Go değerlerini JSON'a çevirir ve JSON'ı Go değerlerine okur. İki temel fonksiyon vardır: Marshal (Go → JSON) ve Unmarshal (JSON → Go).

main.go
package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	City    string `json:"city"`
	Country string `json:"country"`
}

type User struct {
	ID        int      `json:"id"`
	Name      string   `json:"name"`
	Email     string   `json:"email"`
	Tags      []string `json:"tags,omitempty"` // boşsa çıktıya konmaz
	Address   *Address `json:"address,omitempty"`
	Password  string   `json:"-"` // asla JSON'a girmez
	createdAt string   // dışa açık değil: yok sayılır
}

func main() {
	u := User{
		ID:        7,
		Name:      "Zeynep",
		Email:     "[email protected]",
		Address:   &Address{City: "Ankara", Country: "Türkiye"},
		Password:  "gizli",
		createdAt: "2026-01-01",
	}

	data, err := json.Marshal(u)
	if err != nil {
		fmt.Println("hata:", err)
		return
	}
	fmt.Println(string(data))

	// Okunabilir çıktı
	pretty, _ := json.MarshalIndent(u, "", "  ")
	fmt.Println(string(pretty))

	// JSON → Go
	input := `{"id":9,"name":"Ali","email":"[email protected]","tags":["admin","beta"]}`
	var decoded User
	if err := json.Unmarshal([]byte(input), &decoded); err != nil {
		fmt.Println("çözme hatası:", err)
		return
	}
	fmt.Printf("%d %s %v adres:%v\n", decoded.ID, decoded.Name, decoded.Tags, decoded.Address)
}
Çıktı
{"id":7,"name":"Zeynep","email":"[email protected]","address":{"city":"Ankara","country":"Türkiye"}}
{
  "id": 7,
  "name": "Zeynep",
  "email": "[email protected]",
  "address": {
    "city": "Ankara",
    "country": "Türkiye"
  }
}
9 Ali [admin beta] adres:<nil>

Birkaç kural JSON dönüşümünü öngörülebilir kılar:

Yalnızca dışa açık alanlar dönüştürülür. Küçük harfle başlayan alanlar hem yazılırken hem okunurken yok sayılır — hata da vermez.

Unmarshal işaretçi ister. json.Unmarshal(data, &u) yazmazsan değer kopyalanır ve hiçbir şey değişmez. Bu, json: Unmarshal(non-pointer User) hatasıyla sonuçlanır.

Eksik alanlar sıfır değerde kalır. JSON'da olmayan bir alan hata üretmez. Bu yüzden "alan yok" ile "alan sıfır" durumlarını ayırt etmek istiyorsan işaretçi kullanmalısın (*int).

Fazla alanlar sessizce yok sayılır. Katı olmak istiyorsan json.Decoder üzerinde DisallowUnknownFields() çağırırsın.

Etiket seçenekleri

EtiketAnlamı
json:"ad"Alan JSON'da ad olarak görünür
json:"ad,omitempty"Sıfır değerse çıktıya konmaz
json:"-"Alan hiç dönüştürülmez
json:",omitempty"Ad değişmez, yalnızca omitempty uygulanır
json:"ad,string"Sayı veya boolean JSON'da metin olarak yazılır

omitempty'nin "boş" tanımı şudur: sıfır sayı, boş metin, false, nil işaretçi ve uzunluğu sıfır olan dilim/harita. Dikkat: boş bir struct bu tanıma girmez; omitempty iç içe struct değerlerini çıkarmaz. Bu yüzden isteğe bağlı alt nesneler genelde işaretçi olarak tanımlanır.

Okuma sırasında alan eşleştirmesi büyük/küçük harfe duyarsızdır: JSON'daki "Name", "name" ve "NAME" aynı alana gider. Etiket varsa önce etiket denenir.

Harita ve dinamik JSON

Yapısını bilmediğin JSON'ı map[string]any içine okuyabilirsin. Bu esnektir ama tip güvenliğini kaybettirir; sayılar her zaman float64 olarak gelir:

main.go
package main

import (
	"encoding/json"
	"fmt"
	"maps"
	"slices"
)

func main() {
	input := `{
		"ad": "sunucu-1",
		"port": 8080,
		"aktif": true,
		"etiketler": ["prod", "eu"],
		"limitler": {"cpu": 2, "bellek": 512}
	}`

	var m map[string]any
	if err := json.Unmarshal([]byte(input), &m); err != nil {
		fmt.Println("hata:", err)
		return
	}

	for _, k := range slices.Sorted(maps.Keys(m)) {
		fmt.Printf("%-10s %-8T %v\n", k, m[k], m[k])
	}

	// Tip iddiası ile güvenli erişim
	if port, ok := m["port"].(float64); ok {
		fmt.Println("port (int olarak):", int(port))
	}
	if tags, ok := m["etiketler"].([]any); ok {
		fmt.Println("etiket sayısı:", len(tags), "ilki:", tags[0])
	}
	if _, ok := m["yok"]; !ok {
		fmt.Println("beklenen alan eksik")
	}
}
Çıktı
ad         string   sunucu-1
aktif      bool     true
etiketler  []interface {} [prod eu]
limitler   map[string]interface {} map[bellek:512 cpu:2]
port       float64  8080
port (int olarak): 8080
etiket sayısı: 2 ilki: prod
beklenen alan eksik

Sayıların float64 gelmesi klasik bir tuzaktır: Büyük tamsayılar hassasiyet kaybedebilir. Bunun önüne geçmek için ya struct kullanırsın ya da json.Decoder üzerinde UseNumber() çağırıp sayıları json.Number (metin tabanlı) olarak alırsın.

Mümkün olduğunda struct kullan. Struct, beklenen şekli belgeler, derleyicinin alan adlarını denetlemesini sağlar ve dönüşüm hatalarını anlamlı hâle getirir.

Akış hâlinde JSON: Decoder ve Encoder

Marshal/Unmarshal tüm veriyi belleğe alır. Büyük dosyalarda veya ağ akışlarında json.Decoder ve json.Encoder kullanılır; bunlar doğrudan bir io.Reader/io.Writer üzerinde çalışır:

main.go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
)

type Event struct {
	Type  string `json:"type"`
	Count int    `json:"count"`
}

func main() {
	// JSON Lines: her satırda bağımsız bir JSON nesnesi
	stream := `{"type":"click","count":3}
{"type":"view","count":17}
{"type":"purchase","count":1}
`

	dec := json.NewDecoder(strings.NewReader(stream))
	total := 0
	for {
		var e Event
		if err := dec.Decode(&e); err == io.EOF {
			break
		} else if err != nil {
			fmt.Println("çözme hatası:", err)
			return
		}
		fmt.Printf("%-9s %d\n", e.Type, e.Count)
		total += e.Count
	}
	fmt.Println("toplam:", total)

	// Bilinmeyen alanları reddet
	strict := json.NewDecoder(strings.NewReader(`{"type":"click","count":1,"extra":true}`))
	strict.DisallowUnknownFields()
	var e Event
	err := strict.Decode(&e)
	fmt.Println("katı çözücü hatası:", err != nil)

	// Encoder: doğrudan bir Writer'a yazar
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	_ = enc.Encode(Event{Type: "summary", Count: total})
}
Çıktı
click     3
view      17
purchase  1
toplam: 21
katı çözücü hatası: true
{
  "type": "summary",
  "count": 21
}

Decoder, akıştaki JSON değerlerini sırayla okur ve akış bittiğinde io.EOF döndürür. Bu, günlük kayıtları ve olay akışlarında yaygın olan "JSON Lines" biçimini işlemenin doğal yoludur. Encoder.Encode ise her değerden sonra bir satır sonu ekler — bu yüzden aynı biçimi üretmek için idealdir.

Tasarım notları

Giriş/çıkış kodu yazarken alınacak kararlar, kodun ne kadar test edilebilir ve ne kadar dayanıklı olacağını belirler. Birkaç pratik ilke:

Somut tip yerine arayüz al. Bir fonksiyon dosya yolu yerine bir okuyucu alırsa, testte bellekteki bir metinle çağırabilirsin. Dosya açma işini çağırana bırakmak, fonksiyonu hem daha genel hem daha sınanabilir yapar.

Belleğe sığmayacak veriyi belleğe alma. Tüm dosyayı okumak basit ve hızlıdır; ama girdinin boyutunu kontrol etmiyorsan bir gün sizi üzecek bir karardır. Boyut sınırı belli değilse akış tabanlı işleme geç.

Hataları bağlamıyla sarmala. "dosya bulunamadı" mesajı tek başına işe yaramaz; hangi dosya, hangi işlem sırasında? Her katmanın bildiği bilgiyi eklemesi, günlük kayıtlarını okunur kılar.

Kısmi yazmayı düşün. Bir dosyayı üzerine yazarken program ortada çökerse, dosya yarı yazılmış hâlde kalır. Kritik verilerde yaygın çözüm, geçici bir dosyaya yazıp işlem bitince adını değiştirmektir; ad değiştirme işlemi atomiktir.

JSON şemasını struct ile belgele. Dinamik haritalar hızlı bir çözüm gibi görünür ama beklenen şekli kimse bilmez. Struct, hem belge hem doğrulama görevi görür.

Zaman ve para gibi alanlarda biçimi netleştir. Zaman damgalarını RFC 3339 biçiminde, parayı en küçük birimde tamsayı olarak taşımak, ileride yaşanacak pek çok tartışmayı baştan bitirir.

Dosya yolları ve taşınabilirlik

Dosya yollarını elle birleştirmek, farklı işletim sistemlerinde çalışan programlarda en sık karşılaşılan sorunlardan biridir. Ayırıcı karakter her yerde aynı değildir ve yolları metin olarak yapıştırmak, çift ayırıcı ya da eksik ayırıcı gibi sinsi hatalara yol açar. Standart kütüphane bunun için ayrı bir paket sunar: Yolları birleştirir, temizler, dosya adını ve uzantısını ayırır, göreli yolu mutlak yola çevirir.

İkinci bir ayrım, dosya sistemi yolları ile URL yolları arasındadır. İkisi benzer görünür ama kuralları farklıdır; birini diğeri için kullanmak, Windows'ta çalışmayan ya da güvenlik açığı doğuran kodlar üretir. Kullanıcıdan gelen bir yolu doğrudan dosya sisteminde kullanmak özellikle tehlikelidir: Girdi içinde üst dizine çıkan parçalar varsa, program erişmemesi gereken dosyaları okuyabilir. Böyle durumlarda yolu temizlemek ve beklenen kök dizinin altında kaldığını doğrulamak gerekir.

Üçüncü konu, dizinlerde gezinmedir. Bir ağacı baştan sona dolaşmak için elle özyineleme yazmana gerek yoktur; standart kütüphane bu iş için hazır bir gezgin sunar ve her girdide sana dosya bilgisini verir. Büyük ağaçlarda bu yaklaşım hem daha hızlı hem daha az bellek kullanır, çünkü tüm girdileri önceden toplamaz.

Son olarak dosya izinlerini hatırla. Yeni oluşturulan dosyalara verdiğin izin sayısı, sistemin varsayılan maskesiyle birleşir; yani istediğinden daha kısıtlı bir sonuç alabilirsin. Hassas veri yazıyorsan izinleri açıkça daraltmak, yazdıktan sonra kontrol etmek ve dosyayı geçici bir adla oluşturup son adına taşımak iyi alışkanlıklardır.

Sık yapılan hatalar

  • defer f.Close() yazmayı unutmak. Uzun çalışan programlarda dosya tanıtıcıları tükenir.
  • scanner.Err() kontrolünü atlamak. Okuma hatasıyla dosya sonunu ayırt edemezsin.
  • 64 KB'tan uzun satırları hesaba katmamak. bufio.Scanner varsayılan tamponu aşınca durur.
  • Unmarshal'a işaretçi vermemek. Değer kopyalanır ve hiçbir şey dolmaz.
  • Küçük harfli alanların JSON'a gireceğini sanmak. Yalnızca dışa açık alanlar dönüştürülür.
  • omitempty'nin iç içe struct'ları çıkaracağını sanmak. Boş struct "boş" sayılmaz; işaretçi kullan.
  • Dinamik JSON'da sayıların int geleceğini varsaymak. Varsayılan olarak float64 gelir.
  • Yazma sonrası Close hatasını yok saymak. Veri kaybı sessizce gerçekleşebilir.

Alıştırmalar

Alıştırma·Satır ve kelime sayacı
Kolay

Bir metni satır satır işleyip satır sayısı, kelime sayısı ve karakter (rune) sayısını hesaplayan bir program yaz. Girdiyi bir dosyaya yazıp geri okuyarak çalıştır ve en uzun satırı da bul.

İpucu

bufio.Scanner ile satırları gez; kelimeler için strings.Fields, karakterler için utf8.RuneCountInString kullan.

Çözümü göster
main.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
	"unicode/utf8"
)

func main() {
	const path = "metin.txt"
	content := `Go basit bir dildir
eşzamanlılık dilin içindedir
standart kütüphane güçlüdür
`
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		fmt.Println(err)
		return
	}
	defer os.Remove(path)

	f, err := os.Open(path)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()

	var lines, words, runes int
	longest := ""

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		lines++
		words += len(strings.Fields(line))
		runes += utf8.RuneCountInString(line)
		if utf8.RuneCountInString(line) > utf8.RuneCountInString(longest) {
			longest = line
		}
	}
	if err := scanner.Err(); err != nil {
		fmt.Println("tarama hatası:", err)
		return
	}

	fmt.Println("satır:", lines)
	fmt.Println("kelime:", words)
	fmt.Println("karakter:", runes)
	fmt.Printf("en uzun satır (%d karakter): %q\n", utf8.RuneCountInString(longest), longest)
}
Çıktı
satır: 3
kelime: 10
karakter: 74
en uzun satır (28 karakter): "eşzamanlılık dilin içindedir"

Karakter sayarken len(line) değil utf8.RuneCountInString(line) kullandık: Türkçe karakterler UTF-8'de birden fazla bayt tutar. Bu ayrımı Stringler ve Rune'lar dersinde ayrıntısıyla bulabilirsin.

Alıştırma·JSON yapılandırma okuyucu
Orta

Bir uygulama yapılandırmasını temsil eden bir struct tanımla (sunucu adresi, port, hata ayıklama bayrağı, isteğe bağlı zaman aşımı ve etiket listesi). JSON'dan oku, eksik alanlara varsayılan değer ata ve doğrulama yap. Hem eksiksiz hem eksik bir JSON ile dene.

İpucu

"Alan yok" ile "alan sıfır" durumunu ayırt etmek için isteğe bağlı alanları işaretçi (*int) yap. Varsayılanları Unmarshal sonrası uygula.

Çözümü göster
main.go
package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type Config struct {
	Host     string   `json:"host"`
	Port     int      `json:"port"`
	Debug    bool     `json:"debug"`
	TimeoutS *int     `json:"timeout_seconds,omitempty"` // yok / 0 ayrımı için işaretçi
	Tags     []string `json:"tags,omitempty"`
}

func Load(data []byte) (Config, error) {
	var c Config
	if err := json.Unmarshal(data, &c); err != nil {
		return Config{}, fmt.Errorf("yapılandırma çözülemedi: %w", err)
	}

	// Varsayılanlar
	if c.Host == "" {
		c.Host = "localhost"
	}
	if c.Port == 0 {
		c.Port = 8080
	}
	if c.TimeoutS == nil {
		def := 30
		c.TimeoutS = &def
	}

	// Doğrulama
	if c.Port < 1 || c.Port > 65535 {
		return Config{}, errors.New("port 1-65535 aralığında olmalı")
	}
	if *c.TimeoutS < 0 {
		return Config{}, errors.New("zaman aşımı negatif olamaz")
	}
	return c, nil
}

func (c Config) String() string {
	return fmt.Sprintf("%s:%d debug=%t timeout=%ds tags=%v",
		c.Host, c.Port, c.Debug, *c.TimeoutS, c.Tags)
}

func main() {
	full := `{"host":"api.example.com","port":9090,"debug":true,"timeout_seconds":5,"tags":["prod"]}`
	minimal := `{"debug":true}`
	explicitZero := `{"timeout_seconds":0}`
	invalid := `{"port":99999}`

	for _, input := range []string{full, minimal, explicitZero, invalid} {
		cfg, err := Load([]byte(input))
		if err != nil {
			fmt.Println("hata:", err)
			continue
		}
		fmt.Println(cfg)
	}
}
Çıktı
api.example.com:9090 debug=true timeout=5s tags=[prod]
localhost:8080 debug=true timeout=30s tags=[]
localhost:8080 debug=false timeout=0s tags=[]
hata: port 1-65535 aralığında olmalı

*int kullanmanın faydası üçüncü örnekte görünüyor: JSON'da "timeout_seconds":0 açıkça verildiğinde varsayılan uygulanmıyor, sıfır korunuyor. Sade bir int kullansaydın bu iki durumu ayırt etmen imkânsız olurdu. Aynı desen, kısmi güncelleme yapan API'lerde de kullanılır.

Alıştırma·JSON Lines işleme hattı
Zor

Her satırında bir JSON nesnesi bulunan bir günlük dosyası oluştur, json.Decoder ile akış hâlinde oku, olay türlerine göre topla ve sonucu yine JSON olarak yaz. Bozuk satırları atlayıp sayısını raporla. Tüm dosyayı belleğe almadan çalıştığından emin ol.

İpucu

json.Decoder art arda gelen JSON değerlerini okur. Bozuk bir satırda Decode hata döndürür; akışı toparlamak için satır satır okuyup her satırı ayrı ayrı çözmek daha dayanıklıdır.

Çözümü göster
main.go
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"maps"
	"os"
	"slices"
)

type Event struct {
	User   string `json:"user"`
	Action string `json:"action"`
	Amount int    `json:"amount"`
}

type Summary struct {
	TotalEvents  int            `json:"total_events"`
	SkippedLines int            `json:"skipped_lines"`
	ByAction     map[string]int `json:"by_action"`
	TotalAmount  int            `json:"total_amount"`
}

func main() {
	const path = "olaylar.jsonl"
	content := `{"user":"ali","action":"view","amount":0}
{"user":"ayse","action":"purchase","amount":250}
bozuk satır
{"user":"ali","action":"purchase","amount":120}
{"user":"can","action":"view","amount":0}
{"user":"ayse","action":"refund","amount":-50}
{ bu da bozuk
{"user":"can","action":"purchase","amount":75}
`
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		fmt.Println(err)
		return
	}
	defer os.Remove(path)

	f, err := os.Open(path)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()

	summary := Summary{ByAction: map[string]int{}}

	// Satır satır okumak, bozuk bir satırın tüm akışı bozmasını engeller.
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Bytes()
		if len(line) == 0 {
			continue
		}
		var e Event
		if err := json.Unmarshal(line, &e); err != nil {
			summary.SkippedLines++
			continue
		}
		summary.TotalEvents++
		summary.ByAction[e.Action]++
		summary.TotalAmount += e.Amount
	}
	if err := scanner.Err(); err != nil {
		fmt.Println("tarama hatası:", err)
		return
	}

	fmt.Println("eylem türleri:", slices.Sorted(maps.Keys(summary.ByAction)))

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(summary); err != nil {
		fmt.Println("kodlama hatası:", err)
	}
}
Çıktı
eylem türleri: [purchase refund view]
{
  "total_events": 6,
  "skipped_lines": 2,
  "by_action": {
    "purchase": 3,
    "refund": 1,
    "view": 2
  },
  "total_amount": 395
}

Bellek kullanımı girdi boyutundan bağımsızdır: Her an bellekte yalnızca bir satır ve küçük bir özet vardır. Gigabaytlık günlük dosyalarını böyle işleyebilirsin.

Bozuk satırları atlayıp saymak, gerçek sistemlerde yaygın bir stratejidir: Tek bir bozuk kayıt yüzünden tüm işlemi durdurmak genelde istenmez, ama sessizce yok saymak da tehlikelidir. Atlanan kayıt sayısını raporlamak ikisinin ortasıdır. json.Encoder'ın çıktısını doğrudan standart çıktıya yazması, sonucu bir dosyaya ya da ağ bağlantısına yönlendirmeyi tek satırlık bir değişiklik hâline getirir.

Kısa sınav

Kısa sınav

io.Reader arayüzü kaç metot içerir?

bufio.Scanner kullanırken scanner.Err() neden kontrol edilmelidir?

Bir struct alanı küçük harfle başlıyorsa JSON dönüşümünde ne olur?

json:"ad,omitempty" etiketi hangi durumda alanı çıktıdan çıkarır?

map[string]any içine çözülen bir JSON sayısı hangi Go tipinde gelir?

json.Decoder, json.Unmarshala göre hangi avantajı sağlar?

Özet

  • io.Reader ve io.Writer tek metotlu arayüzlerdir ve Go'daki tüm giriş/çıkış bunların üzerine kurulur; fonksiyonlarında somut tip yerine bunları iste.
  • Küçük dosyalar için os.ReadFile/os.WriteFile, büyük dosyalar için os.Open + bufio.Scanner kullanılır.
  • scanner.Err() kontrol edilmeli; varsayılan satır sınırı 64 KB'tır.
  • Açtığın her kaynağı defer ile kapat; yazma yaparken Close hatasını da değerlendir.
  • json.Marshal/Unmarshal yalnızca dışa açık alanları işler; Unmarshal işaretçi ister.
  • Etiketler alan adını, omitempty davranışını ve alanın hiç dönüştürülmemesini (-) belirler.
  • Dinamik JSON'da sayılar float64 gelir; mümkün olduğunda struct kullan.
  • json.Decoder/Encoder akış hâlinde çalışır ve JSON Lines biçimini doğal olarak destekler.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.