// Chapter 35 example: notes that survive a restart. It reads and writes // notes.json in the folder you run it from. // Run with: go run exercises/35/05-notes-saved.go package main import ( "bytes" "encoding/json" "errors" "fmt" "html/template" "net/http" "os" "slices" "strings" "sync" "unicode/utf8" ) const maxNoteLength = 200 const notesFile = "notes.json" type note struct { Text string } // mu protects notes. Any code that reads or changes notes must hold mu. var mu sync.Mutex var notes = []note{} // allNotes returns a copy, so that the caller does not need mu to read it. func allNotes() []note { mu.Lock() defer mu.Unlock() return slices.Clone(notes) } // loadNotes reads the file. A file that does not exist yet means no notes, which // is normal on the first run. Any other problem is an error. func loadNotes() ([]note, error) { data, err := os.ReadFile(notesFile) if errors.Is(err, os.ErrNotExist) { return []note{}, nil } if err != nil { return nil, err } var loaded []note err = json.Unmarshal(data, &loaded) if err != nil { return nil, fmt.Errorf("%s is not valid: %v", notesFile, err) } return loaded, nil } // saveNotes writes a temporary file, then renames it over the real one. The // caller must hold mu, so that only one request writes at a time. func saveNotes(list []note) error { data, err := json.MarshalIndent(list, "", " ") if err != nil { return err } temporary := notesFile + ".tmp" err = os.WriteFile(temporary, data, 0644) if err != nil { return err } return os.Rename(temporary, notesFile) } // addNote saves first, and changes the slice only if saving worked. That way the // slice and the file never disagree. func addNote(n note) error { mu.Lock() defer mu.Unlock() updated := append(slices.Clone(notes), n) err := saveNotes(updated) if err != nil { return err } notes = updated return nil } const notesText = `