קריאה וכתיבה של קובץ שלם
os.WriteFile ו-os.ReadFile מכסות את רוב הצרכים. הן פותחות, כותבות או קוראות, וסוגרות בקריאה אחת.
הדוגמאות בדף הזה עובדות בתיקייה זמנית מ-os.MkdirTemp ומוחקות אותה עם defer os.RemoveAll(dir), כך שהן לא משאירות שום דבר אחריהן. בקוד שלכם, נתיב יחסי כמו "config.json" נפתר ביחס לתיקיית העבודה של התהליך, שהיא לא בהכרח התיקייה של קובץ המקור או של הקובץ הבינארי.
os.WriteFile יוצרת את הקובץ אם צריך, ומרוקנת אותו אם הוא קיים. הארגומנט השלישי הוא הרשאת Unix לקובץ שנוצר מחדש: 0o644 פירושו שהבעלים יכול לקרוא ולכתוב, וכל השאר יכולים לקרוא. היא נזנחת עבור קובץ שכבר קיים, וה-umask של התהליך עשוי להסיר ביטים.
os.ReadFile קוראת הכול לזיכרון. זה נכון לקבצי קונפיגורציה ולקלטים קטנים, ושגוי ללוג של כמה גיגה-בייטים.
קריאה שורה אחרי שורה עם bufio.Scanner
לקבצים גדולים, או כשבכל מקרה רוצים שורות, השתמשו ב-bufio.Scanner. הוא קורא במקטעים ומגיש לכם שורה אחת בכל פעם, בלי תו השורה החדשה.
שלושה פרטים:
- בדקו את
sc.Err()אחרי הלולאה.Scanמחזירהfalseגם בסוף הקובץ וגם בשגיאה, ורקErrמבחינה ביניהם. - מגבלת השורה של 64 KB. כברירת מחדל, שורה בודדת ארוכה מ-64 KB עוצרת את הסורק עם
bufio.Scanner: token too long. לקבצים עם שורות ארוכות (JSON מכווץ, חלק מהלוגים), הגדילו את המגבלה לפני הלולאה:sc.Buffer(make([]byte, 1024*1024), 10*1024*1024). - יחידות אחרות.
sc.Split(bufio.ScanWords)מחזירה מילים;bufio.ScanRunesמחזירה תווים.
לקריאת stream במקטעים בגודל קבוע במקום בשורות, השתמשו ב-f.Read(buf) בלולאה או ב-io.Copy ל-writer אחר.
כתיבה: os.Create, os.OpenFile והוספה לסוף
os.Create(name) פותחת קובץ לכתיבה, ויוצרת או מרוקנת אותו. os.OpenFile נותנת שליטה מלאה באמצעות דגלים:
| דגל | משמעות |
|---|---|
os.O_RDONLY, os.O_WRONLY, os.O_RDWR | פתיחה לקריאה, לכתיבה או לשתיהן (בחרו אחד) |
os.O_CREATE | יצירת הקובץ אם הוא לא קיים |
os.O_TRUNC | ריקון הקובץ בזמן הפתיחה |
os.O_APPEND | כל כתיבה מגיעה לסוף |
os.O_EXCL | יחד עם O_CREATE: כישלון אם הקובץ כבר קיים |
os.Open(name) שקולה ל-OpenFile(name, O_RDONLY, 0). os.Create(name) שקולה ל-OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0o666).
בכתיבה, השגיאה של Close חשובה. חלק ממערכות הקבצים מדווחות על כשלי כתיבה רק בזמן הסגירה, ולכן defer f.Close() לבדו יכול להסתיר נתונים שאבדו. בקבצים שאתם כותבים אליהם, בדקו את Close במפורש כמו ש-appendLine עושה. בקבצים שרק קוראים מהם, defer f.Close() בסדר.
fmt.Fprintln ושאר פונקציות ההדפסה של fmt מקבלות כל io.Writer, כולל קובץ. bufio.Writer מקבץ כתיבות קטנות בזיכרון. שכחת w.Flush() היא באג קלאסי: התוכנית מסתיימת כרגיל והקילו-בייטים האחרונים אף פעם לא מגיעים לקובץ.
האם הקובץ קיים?
ל-Go אין os.Exists. קראו ל-os.Stat ובדקו את השגיאה:
errors.Is(err, fs.ErrNotExist) הוא הניב העדכני. הוא מחליף את os.IsNotExist(err) הישנה, שלא רואה דרך שגיאות עטופות.
בדיקה לפני פתיחה היא לעיתים קרובות מיותרת וחשופה למרוץ: הקובץ יכול להופיע או להיעלם בין הבדיקה לפתיחה. בדרך כלל פשוט פותחים אותו ומטפלים ב-fs.ErrNotExist שמגיעה מהפתיחה. כדי ליצור קובץ רק אם הוא עוד לא קיים, השתמשו ב-O_CREATE|O_EXCL, שהופך את הבדיקה ואת היצירה לצעד אטומי אחד.
תיקיות
| משימה | פונקציה |
|---|---|
| יצירת תיקייה אחת | os.Mkdir(path, 0o755) |
| יצירת נתיב עם תיקיות האב | os.MkdirAll(path, 0o755) |
| רשימת תוכן תיקייה | os.ReadDir(path) |
| מעבר על עץ | filepath.WalkDir(root, fn) |
| מחיקת קובץ או תיקייה ריקה | os.Remove(path) |
| מחיקת עץ | os.RemoveAll(path) |
| שינוי שם או העברה | os.Rename(old, new) |
| קובץ או תיקייה זמניים | os.CreateTemp("", "prefix-*"), os.MkdirTemp("", "prefix") |
| חיבור חלקי נתיב | filepath.Join(a, b, c) |
השתמשו ב-path/filepath לנתיבים של מערכת הקבצים: היא משתמשת במפריד הנכון למערכת ההפעלה (\ ב-Windows). החבילה path מיועדת לנתיבים שמופרדים בלוכסן, כמו URLs.
Go 1.24 הוסיפה גם את os.Root (os.OpenRoot(dir)), שפותח קבצים רק בתוך תיקייה אחת ודוחה נתיבים שבורחים ממנה עם .. או עם symlinks. השתמשו בו כששמות הקבצים מגיעים ממשתמשים.
טעויות נפוצות
- לא לבדוק שגיאות. כל אחת מהקריאות האלה יכולה להיכשל. קובץ שלא נפתח הוא
nil, וכלRead,WriteאוCloseמאוחרים יותר עליו מחזיריםinvalid argument, מה שמסתיר את הסיבה האמיתית (הקובץ היה חסר, או שההרשאה נדחתה). - שכחת
sc.Err()אחרי לולאת סריקה. שגיאת קריאה נראית כמו סוף קובץ. - שכחת
Flushעלbufio.Writer. סוף הקובץ חסר. - התעלמות מהשגיאה של
Closeאחרי כתיבה. שגיאות כתיבה עשויות להתגלות רק שם. defer f.Close()בתוך לולאה על הרבה קבצים. הקבצים נשארים פתוחים עד שהפונקציה חוזרת, ועלולים להיגמר ה-file descriptors. העבירו את גוף הלולאה לפונקציה כדי שכל קובץ ייסגר בכל איטרציה.- הרשאות שנכתבות בעשרוני.
644אינו0o644. Go קוראת את644כמספר עשרוני, שהוא0o1204, ומציבה ביטים מוזרים.
שאלות נפוצות
איך קוראים קובץ שלם לתוך מחרוזת ב-Go?
data, err := os.ReadFile("notes.txt") מחזירה את התוכן כ-[]byte; המירו עם string(data). היא פותחת, קוראת וסוגרת את הקובץ בשבילכם. השתמשו בה לקבצים שנכנסים בנוחות לזיכרון; לקבצים גדולים, קראו שורה אחרי שורה עם bufio.Scanner.
איך קוראים קובץ שורה אחרי שורה ב-Go?
פתחו את הקובץ עם os.Open, כתבו defer f.Close(), עטפו אותו ב-bufio.NewScanner(f), עברו בלולאה עם for sc.Scan() { line := sc.Text() }, ובדקו את sc.Err() אחרי הלולאה. שורות ארוכות מ-64 KB גורמות לסורק להיכשל עם token too long, אלא אם מגדילים את ה-buffer שלו עם sc.Buffer.
איך מוסיפים לסוף קובץ ב-Go?
פתחו אותו עם os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644), כתבו, ובדקו את השגיאה של Close. O_CREATE יוצר את הקובץ אם הוא לא קיים, ו-O_APPEND גורם לכל כתיבה להגיע לסוף.
איך בודקים אם קובץ קיים ב-Go?
קראו ל-os.Stat(path) ובדקו את השגיאה עם errors.Is(err, fs.ErrNotExist). שגיאה שהיא nil פירושה שהקובץ קיים. כל שגיאה אחרת (למשל permission denied) פירושה שאי אפשר לדעת, ולכן טפלו בה בנפרד ואל תתייחסו אליה כאל "לא קיים".