Menu

קריאה וכתיבה של קבצים ב-Golang: os, bufio ותיקיות

איך קוראים וכותבים קבצים ב-Go: os.ReadFile ו-os.WriteFile, קריאה שורה אחרי שורה עם bufio.Scanner, הוספה לסוף קובץ עם os.OpenFile, בדיקה אם קובץ קיים ועבודה עם תיקיות.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

קריאה וכתיבה של קובץ שלם

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) פירושה שאי אפשר לדעת, ולכן טפלו בה בנפרד ואל תתייחסו אליה כאל "לא קיים".

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל