環境
前言
本文為 Go 官方文件 Writing Web Applications 的學習筆記,實作與原文有些許差異。
建立專案
新增並進入 wiki
資料夾。
新增 main.go
檔:
結構體
首先需要建立一個 Page
結構體,一個 Wiki 頁面由標題(Title)和內容(Body)所組成。由於標準庫 io
期望接收到的是一個位元組切片,因此將 Body
的型別設為 []byte
。
1 2 3 4
| type Page struct { Title string Body []byte }
|
主要方法
引入標準庫 io/ioutil
。
為 Page
結構體新增一個 save()
方法,用來儲存頁面:
1 2 3 4
| func (p *Page) save() error { filename := p.Title + ".txt" return ioutil.WriteFile(filename, p.Body, 0600) }
|
ioutil.WriteFile()
方法的第三個參數表示檔案的存取權限。
再新增一個 load()
方法,用來載入頁面:
1 2 3 4 5
| func load(title string) *Page { filename := title + ".txt" body, _ := ioutil.ReadFile(filename) return &Page{Title: title, Body: body} }
|
由於 Go 的方法可以回傳多個值,所以讓 load()
方法也將錯誤一起回傳:
1 2 3 4 5 6 7 8
| func load(title string) (*Page, error) { filename := title + ".txt" body, err := ioutil.ReadFile(filename) if err != nil { return nil, err } return &Page{Title: title, Body: body}, nil }
|
然後建立一個 main()
方法:
1 2 3 4 5 6
| func main() { p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")} p1.save() p2, _ := load("TestPage") fmt.Println(string(p2.Body)) }
|
執行應用,會產生一個 TestPage.txt
檔。
控制器
引入標準庫 net/http
。
1 2 3 4
| import ( "io/ioutil" "net/http" )
|
新增一個 viewHandler()
方法,用來查看 Wiki 頁面。
1 2 3 4 5 6 7 8 9
| func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, err := load(title) if err != nil { http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body) }
|
r.URL.Path
屬性可以獲取當前 URL 路徑。
新增一個 editHandler()
方法,用來修改 Wiki 頁面。
1 2 3 4 5 6 7 8 9 10 11 12 13
| func editHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/edit/"):] p, err := load(title) if err != nil { p = &Page{Title: title} } fmt.Fprintf(w, "<h1>Editing %s</h1>"+ "<form action=\"/store/%s\" method=\"POST\">"+ "<textarea name=\"body\">%s</textarea><br>"+ "<input type=\"submit\" value=\"Save\">"+ "</form>", p.Title, p.Title, p.Body) }
|
新增一個 storeHandler()
方法,用來儲存 Wiki 頁面。
1 2 3 4 5 6 7
| func storeHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/store/"):] body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} p.save() http.Redirect(w, r, "/view/"+title, http.StatusFound) }
|
路由
引入標準庫 log
。
1 2 3 4 5
| import ( "io/ioutil" "log" "net/http" )
|
將 main.go
檔修改如下:
1 2 3 4 5 6
| func main() { http.HandleFunc("/view/", viewHandler) http.HandleFunc("/edit/", editHandler) http.HandleFunc("/store/", storeHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
|
執行應用。
前往 http://localhost:8080/view/TestPage 瀏覽。
模板
引入標準庫 html/template
。
1 2 3 4 5
| import ( "html/template" "io/ioutil" "net/http" )
|
為了將 HTML 從 Go 程式碼中分離,因此新增一個 renderTemplate()
方法。
1 2 3 4
| func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { t, _ := template.ParseFiles(tmpl + ".html") t.Execute(w, p) }
|
新增一個 view.html
檔:
1 2 3 4 5
| <h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>
|
修改 viewHandler()
方法如下:
1 2 3 4 5
| func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, _ := load(title) renderTemplate(w, "view", p) }
|
新增一個 edit.html
檔:
1 2 3 4 5 6 7 8 9 10
| <h1>Editing {{.Title}}</h1>
<form action="/store/{{.Title}}" method="POST"> <div> <textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea> </div> <div> <input type="submit" value="Save"> </div> </form>
|
修改 editHandler()
方法如下:
1 2 3 4 5 6 7 8
| func editHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/edit/"):] p, err := load(title) if err != nil { p = &Page{Title: title} } renderTemplate(w, "edit", p) }
|
執行應用。
前往 http://localhost:8080/view/TestPage 瀏覽。
錯誤處理
修改 renderTemplate()
方法,不要去忽略錯誤:
1 2 3 4 5 6 7 8 9 10 11
| func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { t, err := template.ParseFiles(tmpl + ".html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } err = t.Execute(w, p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }
|
修改 storeHandler()
方法,不要去忽略錯誤:
1 2 3 4 5 6 7 8 9 10 11
| func storeHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/store/"):] body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} err := p.save() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/view/"+title, http.StatusFound) }
|
快取
定義一個全域變數,讓 ParseFiles()
方法在應用程式啟動後只執行一次。Must()
方法會在 ParseFiles()
方法返回 err
不為 nil
時調用 panic
。
1
| var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
|
使用 ExecuteTemplate()
方法來渲染樣板,修改 renderTemplate()
方法:
1 2 3 4 5 6
| func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { err := templates.ExecuteTemplate(w, tmpl+".html", p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }
|
驗證
引入標準庫 regexp
。
1 2 3 4 5 6 7
| import ( "io/ioutil" "log" "net/http" "regexp" "text/template" )
|
定義一個全域變數,讓路由必須符合特定格式。
1
| var validPath = regexp.MustCompile("^/(view|edit|store)/([a-zA-Z0-9]+)$")
|
引入標準庫 errors
。
1 2 3 4 5 6 7 8
| import ( "errors" "io/ioutil" "log" "net/http" "regexp" "text/template" )
|
新增一個 getTitle()
方法,驗證並取得標題名稱。
1 2 3 4 5 6 7 8
| func getTitle(w http.ResponseWriter, r *http.Request) (string, error) { m := validPath.FindStringSubmatch(r.URL.Path) if m == nil { http.NotFound(w, r) return "", errors.New("Invalid Page Title") } return m[2], nil }
|
修改 viewHandler()
方法:
1 2 3 4 5 6 7 8 9
| func viewHandler(w http.ResponseWriter, r *http.Request) { title, err := getTitle(w, r) p, err := load(title) if err != nil { http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } renderTemplate(w, "view", p) }
|
修改 editHandler()
方法:
1 2 3 4 5 6 7 8
| func editHandler(w http.ResponseWriter, r *http.Request) { title, err := getTitle(w, r) p, err := load(title) if err != nil { p = &Page{Title: title} } renderTemplate(w, "edit", p) }
|
修改 storeHandler()
方法:
1 2 3 4 5 6 7 8 9 10 11
| func storeHandler(w http.ResponseWriter, r *http.Request) { title, err := getTitle(w, r) body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} err = p.save() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/view/"+title, http.StatusFound) }
|
封裝
刪除 getTitle()
方法和標準庫 errors
的引用。新增一個 makeHandler()
方法,它接收一個閉包,這個閉包會在驗證通過後被呼叫。
1 2 3 4 5 6 7 8 9 10
| func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { m := validPath.FindStringSubmatch(r.URL.Path) if m == nil { http.NotFound(w, r) return } fn(w, r, m[2]) } }
|
修改 main()
方法:
1 2 3 4 5 6
| func main() { http.HandleFunc("/view/", makeHandler(viewHandler)) http.HandleFunc("/edit/", makeHandler(editHandler)) http.HandleFunc("/store/", makeHandler(storeHandler)) log.Fatal(http.ListenAndServe(":8080", nil)) }
|
修改 viewHandler()
方法:
1 2 3 4 5 6 7 8
| func viewHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := load(title) if err != nil { http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } renderTemplate(w, "view", p) }
|
修改 editHandler()
方法:
1 2 3 4 5 6 7
| func editHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := load(title) if err != nil { p = &Page{Title: title} } renderTemplate(w, "edit", p) }
|
修改 storeHandler()
方法:
1 2 3 4 5 6 7 8 9 10
| func storeHandler(w http.ResponseWriter, r *http.Request, title string) { body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} err := p.save() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/view/"+title, http.StatusFound) }
|
執行應用。
前往 http://localhost:8080/view/TestPage 瀏覽。
程式碼
參考資料