// 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 = ` Notes

Notes

` var notesTemplate = template.Must(template.New("notes").Parse(notesText)) func handleHome(w http.ResponseWriter, r *http.Request) { var page bytes.Buffer err := notesTemplate.Execute(&page, allNotes()) if err != nil { fmt.Println("Template error:", err) http.Error(w, "The page could not be built.", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") page.WriteTo(w) } func handleAdd(w http.ResponseWriter, r *http.Request) { text := strings.TrimSpace(r.FormValue("text")) if text == "" { http.Error(w, "The note is empty.", http.StatusBadRequest) return } if utf8.RuneCountInString(text) > maxNoteLength { http.Error(w, "The note is too long.", http.StatusBadRequest) return } err := addNote(note{Text: text}) if err != nil { fmt.Println("Save error:", err) http.Error(w, "The note could not be saved.", http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusSeeOther) } func main() { loaded, err := loadNotes() if err != nil { fmt.Println("Could not load the notes:", err) fmt.Println("Fix or remove the file, then start the server again.") return } notes = loaded http.HandleFunc("GET /{$}", handleHome) http.HandleFunc("POST /notes", handleAdd) fmt.Println("Server running at http://localhost:8080") fmt.Println("Press Ctrl+C to stop it.") err = http.ListenAndServe("localhost:8080", nil) if err != nil { fmt.Println("Server stopped:", err) } }