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:
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())
}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:
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))
}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:
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])
}
}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ırYazma 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).
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)
}{"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
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:
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")
}
}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 eksikSayı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:
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})
}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.Scannervarsayı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
intgeleceğini varsaymak. Varsayılan olarakfloat64gelir. - Yazma sonrası
Closehatasını yok saymak. Veri kaybı sessizce gerçekleşebilir.
Alıştırmalar
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
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)
}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.
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
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)
}
}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.
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
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)
}
}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
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.Readerveio.Writertek 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çinos.Open+bufio.Scannerkullanılır. scanner.Err()kontrol edilmeli; varsayılan satır sınırı 64 KB'tır.- Açtığın her kaynağı
deferile kapat; yazma yaparkenClosehatasını da değerlendir. json.Marshal/Unmarshalyalnızca dışa açık alanları işler;Unmarshalişaretçi ister.- Etiketler alan adını,
omitemptydavranışını ve alanın hiç dönüştürülmemesini (-) belirler. - Dinamik JSON'da sayılar
float64gelir; mümkün olduğunda struct kullan. json.Decoder/Encoderakış hâlinde çalışır ve JSON Lines biçimini doğal olarak destekler.