演習a5

日時とタイトルと本文を保存できるメモアプリケーションを作成せよ.

(1)

  • タイトル,日付,内容を格納するデータ型Memoと,Memo型を要素とするスライスMemoSliceを宣言せよ.

(2)

  • Memosをファイルに格納する関数Saveとファイルから読み込む関数LoadMemoを作成せよ.
  • ただし,メモファイルのファイル名は memo.txtとし,ファイルの1行目には"Memo File メモ数"という文字列があるものとする.また,ファイルの2行目から,タイトル行,日時行,内容行が,新しい順に並ぶものとする.

(3)

  • (1)(2)を使ってメモアプリを作成せよ.

解答例(1)

ソースコード

  1. package main
  2. package main
  3. import (
  4. "text/template"
  5. "net/http"
  6. "time"
  7. "fmt"
  8. "bufio"
  9. "strconv"
  10. "strings"
  11. "os"
  12. "log"
  13. )
  14. type Memo struct {
  15. Title string
  16. Date string
  17. Body string
  18. }
  19. type MemoSlice []Memo
  20. const EXPAND = 5
  21. func LoadMemo() (memoArray MemoSlice, num int, err error) {
  22. f, err := os.Open("memo.txt")
  23. if err != nil { log.Fatal(err) }
  24. defer func () {
  25. err := f.Close()
  26. if err != nil { log.Fatal(err) }
  27. }()
  28. input := bufio.NewReader(f)
  29. line, _ := input.ReadString('\n')
  30. if line[0:10] != "Memo File " { log.Fatal("LoadMemo: memo.txt の形式が違います.") }
  31. num, _ = strconv.Atoi(line[10:len(line)-1])
  32. memoArray = make([]Memo, num+EXPAND)
  33. memos := memoArray[EXPAND:]
  34. for i:=0; i < num; i++ {
  35. line, err = input.ReadString('\n')
  36. memos[i].Title = line[:len(line)-1]
  37. line, err = input.ReadString('\n')
  38. memos[i].Date = line[:len(line)-1]
  39. line, err = input.ReadString('\n')
  40. memos[i].Body = line[:len(line)-1]
  41. }
  42. return
  43. }
  44. func (memos MemoSlice) Save() error {
  45. f, err := os.OpenFile("memo.txt",os.O_WRONLY,0)
  46. if err != nil { log.Fatal(err) }
  47. defer func() {
  48. err = f.Close()
  49. if err != nil { log.Fatal(err) }
  50. }()
  51. output := bufio.NewWriter(f)
  52. _, err = output.WriteString("Memo File "+strconv.Itoa(len(memos))+"\n")
  53. for i :=0; i < len(memos); i++ {
  54. _, err = output.WriteString(memos[i].Title+"\n")
  55. _, err = output.WriteString(memos[i].Date+"\n")
  56. _, err = output.WriteString(memos[i].Body+"\n")
  57. }
  58. output.Flush()
  59. return err
  60. }
  61. func main() {
  62. fmt.Println("メモサーバ スタート")
  63. memoArray, num, err := LoadMemo()
  64. if err != nil { log.Fatal(err) }
  65. memos := memoArray[len(memoArray)-num:]
  66. fmt.Println("メモファイル読み込み完了")
  67. http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
  68. w.Header().Set("Content-Type", "text/html")
  69. t := template.Must(template.ParseFiles("memo.html"))
  70. err := t.Execute(w,memos)
  71. if err != nil {
  72. fmt.Println(err.Error())
  73. }
  74. })
  75. http.HandleFunc("/memo", func (w http.ResponseWriter, r *http.Request) {
  76. fmt.Println("メモした")
  77. title := r.FormValue("title")
  78. date := time.Now().Format(time.RFC3339)
  79. body := strings.Replace(r.FormValue("body"),"\n","<br />", -1)
  80. memos = memoArray[len(memoArray)-len(memos)-1:]
  81. memos[0] = Memo{title, date, body}
  82. if len(memoArray) == len(memos) {
  83. newMemoArray := make([]Memo,len(memoArray)+EXPAND)
  84. copy(newMemoArray[EXPAND:], memos)
  85. memoArray = newMemoArray
  86. err = memos.Save()
  87. if err != nil { log.Fatal(err) }
  88. fmt.Println("セーブした")
  89. }
  90. http.Redirect(w, r, "/", http.StatusFound)
  91. })
  92. http.HandleFunc("/quit", func (w http.ResponseWriter, r *http.Request) {
  93. err = memos.Save()
  94. if err != nil { log.Fatal(err) }
  95. fmt.Println("セーブした")
  96. fmt.Println("サービス終了")
  97. os.Exit(0)
  98. })
  99. fmt.Println("サービス開始")
  100. http.ListenAndServe(":10080",nil)
  101. }

HTMLテンプレート

  1. <!DOCTYPE htm>
  2. <head>
  3. <meta charset="utf-8" />
  4. <title>memo</title>
  5. </head>
  6. <body>
  7. <h1>メモ</h1>
  8. <form action="/memo" method="POST">
  9. <div>タイトル<br><textarea name="title" rows="1" cols="40"></textarea></div>
  10. <div>内容<br><textarea name="body" rows="3" cols="40"></textarea></div>
  11. <div><input type="submit" value="メモする"></div>
  12. </form>
  13. {{range .}}
  14. <hr>
  15. <p>タイトル:{{.Title}}</p>
  16. <p>日時:{{.Date}}</p>
  17. <p>内容:</p>
  18. {{.Body}}
  19. {{end}}
  20. </body>
  21. </html>

解説

(1)

  • 14行目から18行目:データ型Memoの宣言
  • 20行目:データ型MemoSliceの宣言

(2)

LoadMemo

  • 25行目から30行目:ファイルのオープンとクローズ.
  • 33行目:ファイルから1行読み込み.ReadString('\n')で'\n'まで読み込む.
  • 34行目:ファイルの最初の11バイトが"Memo File "か否かで,メモファイルか否かを判定.
  • 35行目:数を読み込み,strconv.Atoiで整数に変換.これにより,numにメモ数が代入される.
  • 37行目から38行目:メモ数+EXPANDの長さのスライスを作成し,スライスの後方に(EXPAND以降に右詰めで)データを読み込むため,その部分のスライスをmemosとする.
  • 39行目から46行目:タイトル行,日時行,内容行の順に,ファイルからメモ数分だけメモをmemosに読み込む

Save

  • 51行目から56行目:ファイルのオープンとクローズ.
  • 59行目:1行目に"Memo File "とメモ数を出力.strconv.Itoaで整数を文字列に変換.
  • 60行目から64行目:メモ数分だけ,メモを出力.
  • 65行目:出力バッファをフラッシュ.

(3)

main

  • 71行目:メモファイルからメモをmemoArrayに読み込む.この時点で,memoArrayにはEXPAND個(0からEXPAND-1)の空きがある.
  • 73行目:memoArrayのメモが入っている部分(スライス)をmemosとする.
  • 76行目以降で,ハンドルを登録している.ハンドルから,memosにアクセスするために,関数リテラルを用いる.
  • 76行目から83行目:パスが"/"のとき,メモの一覧(memos)を表示する.
  • 85行目から101行目:パスが"/memo"のとき,テンプレートのフォームに入力されたメモデータを,その時の日時とともに,memosに追加する.
  • 92行目から99行目:memoArrayがいっぱいになったら,大きなスライスを用意し,そちらにデータをコピーする.また,メモデータをセーブする.
  • 100行目:"/"にリダイレクトする(メモリストを表示する)
  • 103行目から109行目:"/quit"にアクセスしたら,メモデータをセーブして,サーバを停止する.
  • 112行目:サービス開始.
  • メモファイルが大きくなるとsaveに時間がかかる,Ctl-Cで終わらせると最大5つのメモが消えてしまう,websocketを使ってデータのやり取りを減らした方がいい,などなど,改良の余地は多数あります.

テンプレート

  • 8行目から12行目:メモの入力フォーム.
  • 13行目から19行目:メモのリストの表示.
  • 13行目の{{range .}}により,ソースコードの79行目で指定したmemosというスライスの要素に順にアクセスし,要素数だけ14行目から18行目を繰り返し表示する.
  • スライスの要素はMemo型であるので,そのフィールドには,.Title,.Date,.Bodyでアクセスする.