Tarkibga o'tish

Go’da fayllar bilan ishlash

Fayl — ma’lumotni dastur tugaganidan keyin ham saqlash usuli. Konfiguratsiya, log, JSON hujjat, rasm va hisobotlarning barchasi fayl ko‘rinishida saqlanishi mumkin.

Go’da fayllar bilan ishlashda amal muvaffaqiyatsiz tugashi odatiy holat hisoblanadi. Fayl mavjud bo‘lmasligi, dasturda ruxsat yetishmasligi yoki diskda bo‘sh joy qolmasligi mumkin. Shu sabab har bir fayl amali qaytargan error qiymatini tekshirish kerak.

Asosiy paketlar

Fayllar bilan ishlashda quyidagi standart paketlar ko‘p qo‘llanadi:

  • os — fayl yaratish, ochish, o‘qish, yozish, o‘chirish va metadata olish uchun;
  • io — ma’lumot oqimini o‘qish va yozish uchun umumiy interfeyslar beradi;
  • bufio — buferli o‘qish va yozish, jumladan faylni qatorma-qator o‘qish uchun;
  • path/filepath — operatsion tizimga mos fayl yo‘llarini tuzish uchun;
  • encoding/json — JSON ma’lumotini Go qiymatiga va Go qiymatini JSON ko‘rinishiga o‘girish uchun.

Eski kodlarda io/ioutil paketini uchratishingiz mumkin. Uning funksiyalari Go standart kutubxonasidagi os va io paketlariga ko‘chirilgan. Yangi kodda os.ReadFile, os.WriteFile, os.CreateTemp va io.ReadAll kabi funksiyalar ishlatiladi.

Faylga yozish va undan o‘qish

Kichik faylni bir amal bilan yozish uchun os.WriteFile qulay. U fayl mavjud bo‘lmasa yaratadi. Fayl mavjud bo‘lsa, eski tarkibini qisqartirib, yangi ma’lumotni boshidan yozadi.

package main

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

func main() {
    path := "xabar.txt"
    content := []byte("Go fayllarni baytlar ketma-ketligi sifatida o‘qiydi.\n")

    if err := os.WriteFile(path, content, 0o644); err != nil {
        log.Fatal("faylga yozilmadi: ", err)
    }

    data, err := os.ReadFile(path)
    if err != nil {
        log.Fatal("fayl o‘qilmadi: ", err)
    }

    fmt.Print(string(data))
}

Natija:

Go fayllarni baytlar ketma-ketligi sifatida o‘qiydi.

os.WriteFile ma’lumotni []byte ko‘rinishida qabul qiladi. Fayl tizimi matn, rasm yoki boshqa formatni farqlamaydi: uning uchun hammasi baytlar ketma-ketligidir. []byte(...) matnni UTF-8 baytlariga aylantiradi. O‘qishda os.ReadFile ham []byte qaytaradi, string(data) esa uni matn sifatida chiqarishga tayyorlaydi.

0o644 — sakkizlik sanoq sistemasida yozilgan fayl rejimi. Unix oilasidagi tizimlarda egasi o‘qishi va yozishi, qolgan foydalanuvchilar esa faqat o‘qishi mumkin. Yangi faylning yakuniy ruxsati jarayonning umask sozlamasiga ham bog‘liq. Windows bu bitlarni Unix tizimlaridagidek talqin qilmaydi.

Warning

os.WriteFile mavjud fayl tarkibini almashtiradi. Eski ma’lumot kerak bo‘lsa, avval nusxa oling yoki faylni os.O_APPEND rejimida oching.

Qachon os.ReadFile ishlatish kerak?

os.ReadFile faylning butun tarkibini xotiraga yuklaydi. Kichik konfiguratsiya yoki JSON fayli uchun bu sodda va tushunarli yechim. Bir necha gigabaytli log faylida esa xotira sarfi fayl hajmiga yaqin bo‘ladi. Bunday faylni oqim (stream) tarzida, qismlarga bo‘lib o‘qish kerak.

Faylga qo‘shimcha yozish

Mavjud ma’lumotni saqlab, yangi qatorni fayl oxiriga qo‘shish uchun os.OpenFile ishlatiladi.

package main

import (
    "fmt"
    "os"
)

func appendLine(path, line string) error {
    file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
    if err != nil {
        return fmt.Errorf("faylni ochish: %w", err)
    }

    if _, err := file.WriteString(line + "\n"); err != nil {
        _ = file.Close()
        return fmt.Errorf("faylga yozish: %w", err)
    }

    if err := file.Close(); err != nil {
        return fmt.Errorf("faylni yopish: %w", err)
    }
    return nil
}

func main() {
    path := "ilova.log"
    if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
        fmt.Println("eski faylni o‘chirish:", err)
        return
    }

    if err := appendLine(path, "server ishga tushdi"); err != nil {
        fmt.Println("xato:", err)
        return
    }
    if err := appendLine(path, "so‘rov qabul qilindi"); err != nil {
        fmt.Println("xato:", err)
        return
    }

    data, err := os.ReadFile(path)
    if err != nil {
        fmt.Println("xato:", err)
        return
    }
    fmt.Print(string(data))
}

Natija:

server ishga tushdi
so‘rov qabul qilindi

Bu yerda flaglar | bitli OR operatori bilan birlashtirilgan:

  • os.O_APPEND — har bir yozuvni fayl oxiriga qo‘shadi;
  • os.O_CREATE — fayl mavjud bo‘lmasa yaratadi;
  • os.O_WRONLY — faylni faqat yozish uchun ochadi.

Yozuvchi dasturda Close xatosi ham tekshirildi. Ayrim fayl tizimi xatolari buferdagi ma’lumot diskka uzatilayotganda, ya’ni Write yoki Close vaqtida ma’lum bo‘lishi mumkin. Faqat o‘qish uchun ochilgan fayllarda esa defer file.Close() odatda yetarli.

Bir nechta goroutine yoki jarayon bir faylga bir vaqtda yozsa, yozuvlar kutilmagan tartibda kelishi yoki aralashishi mumkin. O_APPEND har qanday murakkab yozuvni to‘liq atomar qiladi deb hisoblamang. Bitta yozuvchi goroutine, mutex yoki maxsus log kutubxonasi orqali yozishni muvofiqlashtirish xavfsizroq.

Faylni qatorma-qator o‘qish

bufio.Scanner matn faylini qatorma-qator o‘qishga mos. U har safar faqat joriy tokenni xotirada ushlab turadi.

package main

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

func main() {
    file, err := os.Open("ilova.log")
    if err != nil {
        fmt.Println("faylni ochish:", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    scanner.Buffer(make([]byte, 64*1024), 1024*1024)

    lineNumber := 1
    for scanner.Scan() {
        fmt.Printf("%d: %s\n", lineNumber, scanner.Text())
        lineNumber++
    }

    if err := scanner.Err(); err != nil {
        fmt.Println("faylni o‘qish:", err)
    }
}

scanner.Scan() keyingi qatorni o‘qiydi. Qator mavjud bo‘lsa true, fayl tugasa yoki xato yuz bersa false qaytaradi. scanner.Text() joriy qatorni qator oxiridagi \n belgisiz qaytaradi. Sikldan keyingi scanner.Err() tekshiruvi faylning oddiy tugashi bilan o‘qish xatosini ajratadi.

Scanner token hajmiga limit qo‘yadi. Misolda Scanner.Buffer maksimal qator hajmini 1 MiB qilib belgiladi. Bundan ham uzun yozuvlar yoki qatorga ajratilmagan binar ma’lumot uchun bufio.Reader, io.Copy yoki file.Read ma’qul.

os.File va fayl kursori

os.Open va os.OpenFile muvaffaqiyatli tugaganda *os.File qaytaradi. Bu qiymat operatsion tizimdagi ochiq fayl resursiga bog‘langan. Har bir ochiq faylda joriy pozitsiya — fayl kursori (offset) mavjud. Read va Write odatda shu pozitsiyadan boshlab, kursorni oldinga siljitadi. Seek kursorni boshqa pozitsiyaga ko‘chiradi.

Ochiq faylni yopmaslik faqat Go xotirasini emas, operatsion tizimdagi cheklangan fayl deskriptorini ham band qiladi. Ko‘p fayl ochadigan uzoq ishlovchi backend dasturida bu too many open files xatosiga olib kelishi mumkin.

Warning

Sikl ichida minglab fayl ochib, har safar defer file.Close() yozish barcha fayllarni funksiya tugaguncha ochiq qoldiradi. Bunday holatda bitta fayl bilan ishlashni alohida funksiyaga ajrating yoki faylni iteratsiya oxirida aniq yoping.

Fayl mavjudligini va turini tekshirish

os.Stat fayl yoki katalog haqidagi os.FileInfo qiymatini qaytaradi. Faqat os.IsNotExist(err)ni tekshirib, boshqa barcha xatolarni “fayl mavjud” deb qabul qilish noto‘g‘ri.

package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    info, err := os.Stat("xabar.txt")
    switch {
    case err == nil:
        fmt.Println("nom:", info.Name())
        fmt.Println("hajm:", info.Size(), "bayt")
        fmt.Println("katalog:", info.IsDir())
        fmt.Println("rejim:", info.Mode())
    case errors.Is(err, os.ErrNotExist):
        fmt.Println("fayl mavjud emas")
    default:
        fmt.Println("fayl haqida ma’lumot olinmadi:", err)
    }
}

os.FileInfoning asosiy metodlari:

  • Name() — faylning bazaviy nomini qaytaradi;
  • Size() — oddiy fayl hajmini baytlarda qaytaradi;
  • Mode() — fayl turi va ruxsat bitlarini qaytaradi;
  • ModTime() — oxirgi o‘zgartirish vaqtini qaytaradi;
  • IsDir() — qiymat katalog bo‘lsa true qaytaradi;
  • Sys() — operatsion tizimga bog‘liq qo‘shimcha metadata beradi.

errors.Is(err, os.ErrNotExist) o‘ralgan xatolarni ham tekshira oladi. Yangi kodda bu shakl ko‘pincha os.IsNotExistdan qulayroq.

Tekshirish va keyingi amal orasida boshqa jarayon faylni o‘zgartirishi mumkin. Shu sabab “avval mavjudligini tekshir, keyin och” ketma-ketligi xatoni tekshirish o‘rnini bosmaydi. Kerakli amalni bajaring va aynan shu amal qaytargan xatoni boshqaring.

Faylni o‘chirish

os.Remove bitta faylni yoki bo‘sh katalogni o‘chiradi:

if err := os.Remove("xabar.txt"); err != nil && !errors.Is(err, os.ErrNotExist) {
    return fmt.Errorf("faylni o‘chirish: %w", err)
}

Bu parcha to‘liq dastur emas: undan foydalanilganda errors va fmt paketlari import qilinadi, kod esa error qaytaradigan funksiya ichida yoziladi. Fayl mavjud bo‘lmasa misol buni muvaffaqiyat deb qabul qiladi. Boshqa xatolar, masalan ruxsat yetishmasligi, yuqoriga qaytariladi.

O‘chirilgan faylni os.Remove orqali tiklab bo‘lmaydi. Tashqaridan kelgan yo‘l bilan ishlaganda fayl aynan ruxsat etilgan katalog ichida ekanini alohida tekshirish kerak.

JSON fayl bilan ishlash

encoding/json paketi JSON hujjatini Go qiymatiga o‘giradi. Quyidagi dastur avval JSON fayl yaratadi, keyin uni oqim tarzida o‘qiydi:

package main

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

type Person struct {
    Name   string   `json:"name"`
    Age    int      `json:"age"`
    Skills []string `json:"skills"`
}

func main() {
    const document = `{
  "name": "Ali",
  "age": 25,
  "skills": ["Go", "PostgreSQL"]
}`

    if err := os.WriteFile("person.json", []byte(document), 0o644); err != nil {
        fmt.Println("JSON faylini yozish:", err)
        return
    }

    file, err := os.Open("person.json")
    if err != nil {
        fmt.Println("JSON faylini ochish:", err)
        return
    }
    defer file.Close()

    var person Person
    decoder := json.NewDecoder(file)
    decoder.DisallowUnknownFields()
    if err := decoder.Decode(&person); err != nil {
        fmt.Println("JSON faylini o‘qish:", err)
        return
    }

    fmt.Println("Ism:", person.Name)
    fmt.Println("Yosh:", person.Age)
    fmt.Println("Ko‘nikmalar:", person.Skills)
}

Natija:

Ism: Ali
Yosh: 25
Ko‘nikmalar: [Go PostgreSQL]

json.NewDecoder(file) ma’lumotni io.Reader orqali o‘qiydi. Decode(&person) natijani o‘zgartirishi uchun personning manzili beriladi. DisallowUnknownFields JSON’da struct’da yo‘q maydon uchrasa xato qaytaradi. Bu konfiguratsiya faylidagi xato yozilgan maydon nomini jim o‘tkazib yubormaslikka yordam beradi.

Struct tag, masalan json:"name", JSON maydoni bilan Go maydoni orasidagi nomni belgilaydi. Tagsiz eksport qilingan Name maydoni Name, name kabi registri farqli kalitlar bilan ham mos kelishi mumkin. Shunga qaramay, tashqi formatni aniq va barqaror ko‘rsatish uchun tag yozish ma’qul. Kichik JSON faylini os.ReadFile va json.Unmarshal bilan ham o‘qish mumkin; katta yoki ketma-ket JSON qiymatlarida Decoder xotirani tejamliroq ishlatadi.

JSON turlari, Marshal, Unmarshal va qat’iy dekodlash Go’da JSON bilan ishlash darsida fayl yoki HTTP’dan mustaqil tushuntiriladi.

Muhim faylni atomar almashtirish

Konfiguratsiya faylini to‘g‘ridan-to‘g‘ri qayta yozish vaqtida dastur to‘xtasa, faylning bir qismi yozilgan holda qolishi mumkin. Amaliy yechim — yangi tarkibni shu katalogdagi vaqtinchalik faylga yozib, keyin asosiy fayl nomiga almashtirish.

package main

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

func writeAtomic(path string, data []byte, perm os.FileMode) error {
    dir := filepath.Dir(path)
    temp, err := os.CreateTemp(dir, ".update-*")
    if err != nil {
        return fmt.Errorf("vaqtinchalik fayl yaratish: %w", err)
    }
    tempName := temp.Name()
    defer os.Remove(tempName)

    if err := temp.Chmod(perm); err != nil {
        _ = temp.Close()
        return fmt.Errorf("ruxsatni o‘rnatish: %w", err)
    }
    if _, err := temp.Write(data); err != nil {
        _ = temp.Close()
        return fmt.Errorf("vaqtinchalik faylga yozish: %w", err)
    }
    if err := temp.Sync(); err != nil {
        _ = temp.Close()
        return fmt.Errorf("faylni diskka uzatish: %w", err)
    }
    if err := temp.Close(); err != nil {
        return fmt.Errorf("vaqtinchalik faylni yopish: %w", err)
    }
    if err := os.Rename(tempName, path); err != nil {
        return fmt.Errorf("asosiy faylni almashtirish: %w", err)
    }
    return nil
}

func main() {
    if err := writeAtomic("config.txt", []byte("port=8080\n"), 0o600); err != nil {
        fmt.Println("xato:", err)
        return
    }

    data, err := os.ReadFile("config.txt")
    if err != nil {
        fmt.Println("xato:", err)
        return
    }
    fmt.Print(string(data))
}

Natija:

port=8080

Vaqtinchalik fayl asosiy fayl bilan bir katalogda yaratiladi. Bu Renamening bir fayl tizimi ichida bajarilish ehtimolini ta’minlaydi. Sync ma’lumotni operatsion tizim buferidan saqlash qurilmasiga uzatishni so‘raydi. Renamening mavjud faylni almashtirish semantikasi operatsion tizimga bog‘liq; ayniqsa Windows va bir nechta jarayon bir faylni ishlatayotgan holat alohida sinovdan o‘tkaziladi. Elektr uzilishiga qarshi kuchli kafolat kerak bo‘lsa, katalogni ham sinxronlash kabi platformaga xos choralar talab qilinishi mumkin.

Yo‘l xavfsizligi

Foydalanuvchi bergan ../../maxfiy.txt kabi yo‘l ruxsat etilgan katalogdan tashqariga chiqishi mumkin. Bu katalog bo‘ylab o‘tish (path traversal) zaifligi deyiladi. filepath.Clean yo‘lni tartiblaydi, lekin uning ruxsat etilgan katalog ichida ekanini kafolatlamaydi. Ishonch chegarasi filepath.Rel yoki ishonchli bazaviy katalog bilan boshqa aniq tekshiruv orqali nazorat qilinishi kerak. Symbolic link ham tekshirilgan yo‘lni tashqaridagi faylga olib borishi mumkin.

Fayl nomini tashqi manbadan oladigan HTTP handler, arxiv ochuvchi dastur va CLI utilitada bu masala xavfsizlik talabi hisoblanadi. Katalog yo‘llari va ularni xavfsiz tekshirish keyingi darsda batafsil ko‘rib chiqiladi.

Keng tarqalgan xatolar

Xatodan keyin bajarishni davom ettirish

os.Open xato qaytarsa, file odatda nil bo‘ladi. Xatoni chiqarib, keyin defer file.Close()ga o‘tish panic keltirib chiqarishi mumkin. Xatodan keyin return qiling yoki xatoni chaqiruvchiga qaytaring.

Har qanday faylni to‘liq xotiraga yuklash

os.ReadFile sodda, lekin xotira sarfi fayl hajmi bilan o‘sadi. Fayl hajmi nazorat qilinmasa, Scanner, Reader, io.Copy yoki o‘lcham limiti ishlating.

Yozish natijasidagi baytlar sonini e’tiborsiz qoldirish

Write yozilgan baytlar soni va xatoni qaytaradi. n < len(data) bo‘lib, xato nil bo‘lsa ham bu qisqa yozuv hisoblanadi. io.WriteString va io.Copy kabi yordamchi funksiyalar bilan ishlaganda ham qaytgan natija tekshiriladi.

Fayl mavjudligini alohida tekshirishga ortiqcha tayanish

Tekshiruvdan so‘ng fayl o‘chishi yoki ruxsati o‘zgarishi mumkin. Bu tekshirish va ishlatish orasidagi poyga holati (TOCTOU) deyiladi. Asosiy amal qaytargan xato doim tekshirilishi shart.

Interviewda nimalarga e’tibor beriladi?

  • os.ReadFile butun faylni xotiraga oladi, oqimli o‘qish esa xotira sarfini cheklaydi.
  • defer file.Close() o‘qishda qulay, yozishda esa Close xatosini aniq tekshirish talab qilinishi mumkin.
  • O_CREATE, O_APPEND, O_TRUNC, O_RDONLY va O_WRONLY flaglari faylning ochilish semantikasini belgilaydi.
  • os.Statdagi “mavjud emas” xatosi boshqa I/O xatolaridan ajratiladi.
  • Atomar yangilash odatda shu katalogdagi vaqtinchalik fayl va Rename orqali quriladi, lekin chidamlilik kafolatlari fayl tizimi va operatsion tizimga bog‘liq.

Xulosa

Bu darsda kichik faylni o‘qish va yozish, fayl oxiriga ma’lumot qo‘shish, katta matnni qatorma-qator qayta ishlash, metadata olish, JSON o‘qish va muhim faylni atomar almashtirishni ko‘rib chiqdik. Amaliy kodda uchta qoida doim muhim: har bir xatoni tekshirish, ochilgan resursni vaqtida yopish va fayl hajmiga mos o‘qish usulini tanlash.

Keyingi darsda katalog yaratish, uning tarkibini o‘qish, katalog daraxti bo‘ylab yurish va fayl yo‘llarini xavfsiz tuzishni o‘rganamiz.