Go standart kutubxonasining hayratlanarli darajada katta qismi — fayllar, tarmoq ulanishlari, siqilgan arxivlar, HTTP so'rov tanalari, hatto xotiradagi buferlar — har biri bitta metoddan iborat bo'lgan ikkita interfeys ustiga qurilgan. Bu minimalizm shunchaki o'z-o'zicha maqsad emas: aynan shu sababli "baytlar oqimi"ni qayta ishlash uchun yozgan funksiyangiz bu oqim diskdagi fayldan, TCP soketidan, gzip dekompressoridan yoki unit testdagi strings.Readerdan kelishidan qat'i nazar bir xil o'zgarishsiz ishlayveradi. io.Reader va io.Writer interfeyslarini ularni erkin kompozitsiya qila oladigan darajada chuqur tushunish Go dasturlashda o'rganilishi kerak bo'lgan eng muhim ko'nikmalardan biridir, chunki butun ekotizim aynan ularga ulanish uchun loyihalangan.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Read metodi chaqiruvchi uzatgan p buferini ma'lumot bilan to'ldiradi va uning qancha bayti to'ldirilganini n orqali xabar qiladi. Bu shartnomaning o'ziga xos nozik jihati bor: oqimda yana ko'proq ma'lumot mavjud bo'lsa va hech qanday xatolik yuz bermagan taqdirda ham, Read chaqiruvi len(p) hajmidan kamroq bayt qaytarishi mumkin. Shu sababli chaqiruvchi dastur bitta chaqiruvda barcha kerakli ma'lumotni oladi deb xomxayol qilmasdan, to'liq o'qish uchun Readni qayta-qayta chaqirishga tayyor turishi kerak. io.EOF (End of File) oqim tugaganini bildiradi va u xavfli xatolik emas; bu Reader oqim yakunlanganini bildiruvchi odatiy va kutilgan signalidir.
reader := strings.NewReader("Hello, World!")
buf := make([]byte, 8)
for {
n, err := reader.Read(buf)
if n > 0 {
fmt.Printf("read %d bytes: %s\n", n, buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
fmt.Println("error:", err)
break
}
}
Tekshiruv tartibiga e'tibor bering: avval n > 0 shartini tekshirib, o'qilgan baytlarni qayta ishlang, so'ngra xatolikni tekshiring. Chunki Readerga bir chaqiruvda ham o'qilgan ma'lumotni, ham io.EOF signalini birgalikda qaytarishga to'liq ruxsat etilgan. Xatolikni birinchi tekshirib, nga qaramasdan io.EOF yuz berganda darhol chiqib ketadigan kod ba'zi readerlar chiqargan ma'lumotning so'nggi bo'lagini jimgina yo'qotib yuboradi. Bu nozik xato odatda yashirin qoladi, chunki standart kutubxonadagi ko'plab readerlar ma'lumot bilan bir vaqtda EOF qaytarmaydi — toki biror maxsus manbaga duch kelmaguncha.
Avval
n > 0ni qayta ishlang, keyin xatoni tekshiring:Readerbir chaqiruvda ham ma'lumotni, hamio.EOFni qaytarishi mumkin.
Writer — buning ko'zgudagi aksi bo'lib, o'zining xavfli jihatiga ega: xatoliksiz qisman yozish (n < len(p)) interfeys tomonidan texnik jihatdan ruxsat etilgan. Shuning uchun faqat errni tekshirib, yozilgan baytlar soni nni e'tiborsiz qoldiradigan kod qisman yozadigan writerlarda ma'lumotni jimgina yo'qotishi mumkin. Amaliyotda ko'pchilik writerlar yo to'liq yozadi, yo xatolik qaytaradi, biroq begona io.Writer implementatsiyalarida — ayniqsa tarmoq soketlarida — bu qoidani yodda tutish juda muhimdir.
Ikkala interfeysning shunchalik ixcham bo'lishining ulkan foydasi — ularning kompozitsiyasidadir: o'nlab turli xil tiplar bir-biri haqida hech narsa bilmasdan ularning talablarini bajaradi. *os.File, *bytes.Buffer, *strings.Reader, HTTP javob tanasi (resp.Body), gzip.Reader — bularning barchasi io.Readerdir. Ushbu interfeys asosida yozilgan funksiya ularning har biri bilan muammosiz ishlaydi. Shu sababli standart kutubxonadagi I/O kodlarining deyarli barchasi aniq strukturalar o'rniga io.Reader/io.Writer interfeyslarini parametr sifatida qabul qiladi.
Standart kutubxonadagi ikkita tip xotirada "bayt yoki matnlarni yig'ish" vazifasini qoplaydi va ulardan to'g'risini tanlash asosan yozilgan ma'lumotni qayta o'qish kerak-yo'qligiga bog'liq.
bytes.Buffer — o'suvchi, o'qiladigan va yoziladigan universal bayt buferidir. U ham io.Reader, ham io.Writer interfeyslarini implementatsiya qiladi. Shu bois xotirada virtual faylga o'xshash xatti-harakat talab qilinadigan barcha holatlarda (io.Reader kutayotgan funksiyaga ma'lumot berish yoki io.Writer kutayotgan funksiya natijasini tutib olish uchun) juda qulaydir:
var buf bytes.Buffer
buf.WriteString("Hello")
buf.WriteByte(' ')
buf.Write([]byte("World"))
fmt.Println(buf.String()) // "Hello World"
out := make([]byte, 5)
buf.Read(out)
fmt.Println(string(out)) // "Hello" — Read o'qilgan qismini buferdan chiqaradi
strings.Builder faqat yozish uchun mo'ljallangan bo'lib, sof satrlarni (string) minimal xotira sarfi bilan qurishga xizmat qiladi: unda Read metodi yo'q, ortiqcha murakkablik yo'q va eng muhimi, oxirida .String() chaqirilganda u bytes.Buffer kabi xotiradagi baytlarni qayta nusxalamasdan (zero-allocation) satrga aylantiradi:
var b strings.Builder
b.WriteString("Hello")
b.WriteByte(' ')
b.WriteString("World")
result := b.String()
Asosiy qoida: agar yakuniy maqsad shunchaki satr hosil qilish bo'lsa va oraliqda buferdan hech kim o'qimasa, strings.Builderdan foydalaning. Agar bir vaqtda ham yozish, ham o'qish kerak bo'lsa yoki io.Reader/io.Writer interfeyslariga moslash zarur bo'lsa, bytes.Bufferni tanlang.
*os.File yoki tarmoq ulanishiga qilingan har bir Read yoki Write amali operatsion tizim darajasidagi tizim chaqiruvini (syscall) keltirib chiqaradi, tizim chaqiruvlari esa oddiy funksiya chaqiruvlaridan yuzlab baravar sekinroq ishlaydi. Fayldan xom os.File.Read orqali bir baytdan o'qish va ming bayt uchun ming marta tizim chaqiruvi amalga oshirish nihoyatda samarasizdir. bufio paketi reader yoki writerni ichki bufer bilan o'rab oladi: natijada qimmat syscall amallari katta bo'laklarda (odatda 4KB yoki 64KB) bajariladi, dastur kodi esa bemalol baytma-bayt yoki qatorma-qator ishlashda davom etaveradi.
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
process(line)
}
if err := scanner.Err(); err != nil {
return err
}
bufio.Scanner sukut bo'yicha matnni qatorlarga ajratadi, ammo uning Split funksiyasini almashtirish mumkin: so'zlar bo'yicha ajratish uchun bufio.ScanWords yoki maxsus formatlar uchun o'zingizning bufio.SplitFunc funksiyangizni berishingiz mumkin. Uning yagona cheklovi — maksimal token o'lchami (sukut bo'yicha 64KB, Buffer metodi orqali oshirish mumkin). Juda uzun bitta qatorga ega bo'lgan fayl xotirani to'ldirib qulatmaslik uchun Scanni "token too long" xatoligi bilan to'xtatadi. Bu nosozlik emas, ataylab qo'yilgan himoya mexanizmidir.
Yozish jarayonida esa shuni yodda tutish kerakki, bufio.Writer ma'lumotlarni o'z buferida to'playdi va to'lmaguncha diskka chiqarmaydi. Flush chaqiruvini unutish — testlarda to'g'ri ko'rinadigan, ammo amalda chala yozilgan yoki bo'sh fayl hosil bo'lishining klassik sababidir:
file, err := os.Create("output.txt")
if err != nil {
return err
}
defer file.Close()
w := bufio.NewWriter(file)
defer w.Flush() // oxirgi qo'yilgan defer BIRINCHI ishga tushadi — shuning uchun file.Close()dan oldin flush bajariladi
w.WriteString("line 1\n")
w.WriteString("line 2\n")
Bu yerdagi defer tartibi bejiz emas: Goda defer LIFO (oxirgi kirgan — birinchi chiqadi) tartibida ishlaydi. Shu bois funksiya yakunlanganda w.Flush() birinchi bo'lib buferdagi qolgan ma'lumotlarni faylga to'kadi, shundan keyingina file.Close() faylni yopadi.
Streaming talab etilmaydigan oddiy vazifalar uchun — butun faylni bir yo'la xotiraga o'qish yoki yozishda — os.ReadFile va os.WriteFile eng ixcham vositalardir:
data, err := os.ReadFile("config.json")
if err != nil {
return err
}
err = os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
Biroq fayl hajmi noma'lum yoki juda katta bo'lganda (katta loglar, foydalanuvchi yuklagan video yoki ma'lumotlar bazasi zaxiralari) "hammasini bir yo'la xotiraga yuklash" operativ xotirani to'ldirib tizimni qulatadi. Bunday hollarda butun fayl o'rniga kichik bo'laklab oqizib qayta ishlash uchun os.Open/os.Create bilan bufio yoki to'g'ridan-to'g'ri io.Copy uslubiga o'tish lozim.
Fayl oxiriga ma'lumot qo'shish (append) uchun esa os.OpenFile bayroqlari ishlatiladi:
f, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
f.WriteString("new log entry\n")
Yuqoridagi barcha misollar berilgan fayl yo'llarini xavfsiz va ishonchli deb hisoblaydi. Ammo yo'l tashqi foydalanuvchi ma'lumotlaridan shakllangan zahoti — masalan, arxiv ichidagi fayl nomi yoki "faylni yuklab olish" so'rovi parametri — bu ishonch jiddiy xavfga aylanadi. Klassik zaiflik turi — bu Path Traversal bo'lib, bunda tajovuzkor ../../etc/passwd kabi yo'llar yoki tashqariga ishora qiluvchi symlinklar orqali tizimning himoyalangan fayllariga kirishga urinadi. Go 1.24 da aynan shu turdagi xavflarning oldini olish uchun os.Root mexanizmi taqdim etildi.
os.OpenRoot(dir) ko'rsatilgan dir papkasiga qat'iy bog'langan *os.Root obyektini qaytaradi. U orqali bajariladigan har qanday amal — Open, Create, Stat va boshqalar — yo'llarni faqat va faqat shu ildiz papka doirasida hal qiladi. Har qanday tashqariga chiqishga urinishlar (.. segmentlari yoki tashqariga yo'naltiruvchi simvolik havolalar) avtomatik xatolik bilan to'xtatiladi:
root, err := os.OpenRoot("/var/uploads")
if err != nil {
return err
}
defer root.Close()
// Yo'l faqat /var/uploads ga nisbatan ochiladi. Agar name o'zgaruvchisida
// "../etc/passwd" bo'lsa yoki ildizdan tashqariga ishora qiluvchi symlink bo'lsa,
// u ruxsatsiz faylni o'qish o'rniga xatolik qaytaradi.
f, err := root.Open(name)
if err != nil {
return err
}
defer f.Close()
Go 1.24 da bu tip faqat o'qish va ochish amallarini qamrab olgan edi. Go 1.25 esa *os.Rootga Chmod, Chown, Chtimes, Lchown, Link, MkdirAll, ReadFile, Readlink, RemoveAll, Rename, Symlink va WriteFile metodlarini qo'shib, uni to'liq sandbox qilingan mustaqil fayl tizimi API'siga aylantirdi. Masalan, ishonchsiz arxivni ochishni endi butunlay Root metodlari orqali bajarish mumkin — sandbox qilinmagan os paketi chaqiruvlarini sandbox qilingan o'qishlar bilan aralashtirmasdan.
Bu yerdan kelib chiqadigan amaliy qoida oddiy: kodingiz fayl yo'lini o'zingiz nazorat qilmaydigan biror manbadan qurgan har qanday joyda, yo'lni qo'lda tekshirish o'rniga os.Root dan foydalaning. Qo'lda tekshirish — bu bir marta nozik xatoga yo'l qo'yish juda oson bo'lgan ish (.. segmentlari, symlinklar, kodlangan belgilar), os.Root esa aynan buni qilishingiz shart bo'lmasligi uchun mavjud.
Bog'liq bo'lgan io/fs paketi Go 1.25 da kichik, ammo muhim qo'shimchani oldi: ReadLinkFS interfeysi (uni os.DirFS ham, Root.FS ham amalga oshiradi). Bundan oldin umumiy io/fs.FS interfeysiga tayanib yozilgan kodda symlinkni aniqlashning — uning nishonini o'qishni aytmaganda ham — ko'chma usuli yo'q edi: symlink bilan ishlaydigan har qanday kod aniq os paketiga tushishga majbur bo'lardi.
io.Copy(dst, src) baytlarni har qanday Readerdan har qanday Writerga ko'chiruvchi eng asosiy ishchi kuchidir. Nusxalash siklini qo'lda yozgandan ko'ra io.Copydan foydalanish ancha afzalroqdir: u ichki buferlashni o'zi optimal hal qiladi va operatsion tizim darajasidagi tezkor yo'llarni (masalan, Linuxdagi sendfile syscall'i) qo'llab-quvvatlaydi:
src, err := os.Open("source.txt")
if err != nil {
return err
}
defer src.Close()
dst, err := os.Create("dest.txt")
if err != nil {
return err
}
defer dst.Close()
written, err := io.Copy(dst, src)
io paketida butun oqimni xotirada to'plamasdan turib murakkab vazifalarni hal qiluvchi ajoyib utilitlar mavjud:
io.LimitReader(r, n) — o'qilishi mumkin bo'lgan maksimal baytlar sonini cheklaydi. Bu tashqi foydalanuvchilar yuboradigan so'rov tanasini o'qishda serverni xotira to'lib qolishidan (DoS hujumlari) asrash uchun zarurdir.io.TeeReader(r, w) — xuddi quvur tarmoqlagichi kabi, o'qilayotgan har bir baytni bir vaqtning o'zida w writer'iga ham yozib boradi. Bu ma'lumot oqib o'tayotgan paytda uni xotirada ikki marta buferlamasdan xeshini (hash) hisoblash yoki log qilish uchun juda qulaydir.io.MultiWriter(w1, w2, ...) — bitta yozish amalini bir vaqtda bir nechta manzilga (masalan, bir vaqtning o'zida faylga ham, konsolga ham) nusxalaydi.io.Pipe() — xotirada sinxron quvur yaratib, uning bir tomonini io.Readerga, ikkinchi tomonini io.Writerga ulaydi. Turli goroutinelar orasida ma'lumotni diskka yoki oraliq xotiraga yozmasdan, to'g'ridan-to'g'ri oqizish uchun ishlatiladi.reader := strings.NewReader("Hello, World!")
var log bytes.Buffer
tee := io.TeeReader(reader, &log)
data, _ := io.ReadAll(tee)
// data == "Hello, World!", yon ta'sir sifatida log.String() == "Hello, World!"
Ushbu utilitlar gzip.Reader yoki multipart.Reader kabi boshqa oqim vositalari bilan bemalol zanjir kabi ulanadi: masalan, LimitReader(gzip.NewReader(TeeReader(conn, hashWriter)), maxSize) orqali bitta oqimda dekompressiya qilish, o'lchamni cheklash va xesh hisoblashni bir yo'la amalga oshirish mumkin.
I/O bilan ishlashda eng muhim dizayn tanlovi: io.ReadAll (yoki os.ReadFile) orqali hamma narsani birdaniga xotiraga yuklashmi yoki bufio/io.Copy orqali bo'laklab oqizishmi? Kichik hajmli konfiguratsiya fayllari yoki oddiy JSON payloadlar uchun to'liq yuklash oddiy va mutlaqo maqbuldir. Biroq hajmi noma'lum yoki tashqi foydalanuvchilar tomonidan boshqariladigan ma'lumotlar bilan ishlashda doimo chegaralangan oqim uslubidan foydalanish lozim.
// O'lchami oldindan ma'lum bo'lgan kichik konfiguratsiyalar uchun qulay:
data, _ := io.ReadAll(configReader)
// Internetdan keluvchi noma'lum hajmdagi so'rovlar uchun albatta cheklov qo'yilishi shart:
body := io.LimitReader(r.Body, 10<<20) // Maksimal 10MB bilan cheklash
data, _ := io.ReadAll(body)
Bitta io.LimitReader o'rami DoS (denial-of-service) zaifliklarining butun bir sinfini bartaraf etadigan ajoyib odat bo'lib, me'yordagi kiruvchi ma'lumotlar uchun hech qanday qo'shimcha xarajat talab qilmaydi. Tashqi manbalardan ma'lumot qabul qilganda har doim oqimni cheklang; cheklovsiz ReadAllni esa faqat o'zingiz to'liq nazorat qiladigan ma'lumotlar uchungina saqlang.
Keyingi bobda aynan shu io.Reader va io.Writer ustiga qurilgan ma'lumotlarni kodlash va serializatsiya qilish usullarini (encoding/json va boshqalar) ko'rib chiqamiz.