All Posts programming Learn Go | Tutorial 6: The Memory Loss Issue (File Persistence)

Learn Go | Tutorial 6: The Memory Loss Issue (File Persistence)

ยท 734 words ยท 4 minute read
go ▹

The final tutorial solves the memory loss problem by introducing persistence. We will write tasks to a simple text file so they survive when the program closes.

Currently, when you type quit, your tasks vanish. This makes your tool useless.

The Need is to save your data to the hard drive before exiting and load it back when starting.

We will use the os package to write a simple file.


Step 1: Saving to a file ๐Ÿ”—

When the user types quit, we shouldn’t just return. We need to take our slice of strings and dump it into a file.

We will create a helper function saveTasks to keep main clean.

Add this function outside of main():

func saveTasks(tasks []string) {
    // 1. Create (or overwrite) a file named "tasks.txt"
    f, err := os.Create("tasks.txt")
    if err != nil {
        fmt.Println("Error creating file:", err)
        return
    }
    // 2. Ensure the file closes when the function finishes
    defer f.Close()

    // 3. Write each task to a new line
    for _, task := range tasks {
        f.WriteString(task + "\n")
    }
}

New Concepts:

  • os.Create("filename"): Creates a file. If it exists, it truncates it (wipes it clean).
  • defer f.Close(): “Defer” schedules this cleanup code to run right before the function exits. It ensures you don’t leave the file locked.
  • f.WriteString(...): Writes text to the open file.

Step 2: Loading from a file ๐Ÿ”—

When the program starts, we need to look for tasks.txt and read it into our slice.

Add this helper function:

func loadTasks() []string {
    // 1. Open the file
    f, err := os.Open("tasks.txt")
    if err != nil {
        // If file doesn't exist, just return an empty list
        return []string{} 
    }
    defer f.Close()

    // 2. Create a scanner to read line by line
    scanner := bufio.NewScanner(f)
    var loadedTasks []string

    // 3. Scan the file
    for scanner.Scan() {
        loadedTasks = append(loadedTasks, scanner.Text())
    }

    return loadedTasks
}

Note: We use bufio.Scanner again! It works exactly the same on a file as it did on os.Stdin (keyboard input).


Step 3: Connecting it all ๐Ÿ”—

Update your main function to:

  1. Call loadTasks() at the very beginning.
  2. Call saveTasks() inside the quit case.

Here is the final, complete code for Go-Getter:

package main

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

// Helper: Writes slice to disk
func saveTasks(tasks []string) {
    f, err := os.Create("tasks.txt")
    if err != nil {
        fmt.Println("Error saving:", err)
        return
    }
    defer f.Close()

    for _, task := range tasks {
        f.WriteString(task + "\n")
    }
    fmt.Println("Tasks saved to tasks.txt")
}

// Helper: Reads from disk
func loadTasks() []string {
    f, err := os.Open("tasks.txt")
    if err != nil {
        return []string{} // Return empty if file missing
    }
    defer f.Close()

    var loaded []string
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        loaded = append(loaded, scanner.Text())
    }
    return loaded
}

func main() {
    // LOAD tasks at startup
    tasks := loadTasks()
    scanner := bufio.NewScanner(os.Stdin)

    fmt.Println("Go-Getter v1.0")
    fmt.Println("---------------------")
    fmt.Printf("Loaded %d tasks.\n", len(tasks))

    for {
        fmt.Print("\nCommand (add, list, quit): ")
        scanner.Scan()
        command := scanner.Text()

        switch command {
        case "add":
            fmt.Print("Enter new task: ")
            scanner.Scan()
            task := scanner.Text()
            if len(task) > 0 {
                tasks = append(tasks, task)
                fmt.Println("Added.")
            }

        case "list":
            if len(tasks) == 0 {
                fmt.Println("No tasks found.")
            } else {
                for i, t := range tasks {
                    fmt.Printf("%d. %s\n", i+1, t)
                }
            }

        case "quit":
            // SAVE tasks before exiting
            saveTasks(tasks)
            fmt.Println("Goodbye!")
            return

        default:
            fmt.Println("Unknown command.")
        }
    }
}

Conclusion ๐Ÿ”—

You now have a fully functional Go program. You learned the syntax not by memorizing a textbook, but by solving specific problems:

  1. Variables because hardcoding strings was annoying.
  2. Slices because variables couldn’t hold lists.
  3. Loops because printing items one by one was tedious.
  4. Scanners because you needed user input.
  5. Files because you needed to save your work.

Where to go next?

  • Structs: Right now your tasks are just strings. If you want to track “Done” status (true/false), you will need a struct like type Task struct { Name string; Done bool }.
  • JSON: Saving to a text file is simple, but json is better for complex data.
  • Web Server: Use net/http to turn this CLI into a web API.

I hope you enjoyed reading this post as much as I enjoyed writing it. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .

go ▹