Writing Web Applications
Giới thiệu
Những nội dung được đề cập trong hướng dẫn này:
- Tạo cấu trúc dữ liệu với các phương thức load và save
- Dùng gói
net/httpđể xây dựng ứng dụng web - Dùng gói
html/templateđể xử lý các template HTML - Dùng gói
regexpđể xác thực đầu vào của người dùng - Dùng closure
Kiến thức được giả định trước:
- Kinh nghiệm lập trình
- Hiểu biết về các công nghệ web cơ bản (HTTP, HTML)
- Một số kiến thức về dòng lệnh UNIX/DOS
Bắt đầu
Hiện tại, bạn cần có máy chạy FreeBSD, Linux, macOS hoặc Windows để chạy Go.
Chúng ta sẽ dùng $ để biểu thị dấu nhắc lệnh.
Cài đặt Go (xem Hướng dẫn cài đặt).
Tạo một thư mục mới cho hướng dẫn này bên trong GOPATH của bạn và cd vào đó:
$ mkdir gowiki $ cd gowiki
Tạo một tệp tên wiki.go, mở trong trình soạn thảo yêu thích của bạn, và
thêm các dòng sau:
package main
import (
"fmt"
"os"
)
Chúng ta import các gói fmt và os từ thư viện chuẩn của Go.
Sau này, khi chúng ta triển khai thêm chức năng, chúng ta sẽ thêm nhiều gói hơn vào
khai báo import này.
Cấu trúc dữ liệu
Hãy bắt đầu bằng cách định nghĩa các cấu trúc dữ liệu. Một wiki bao gồm một chuỗi các
trang liên kết với nhau, mỗi trang có tiêu đề và nội dung (nội dung trang). Ở đây, chúng
ta định nghĩa Page là một struct với hai trường đại diện cho tiêu đề và nội
dung.
type Page struct {
Title string
Body []byte
}
Kiểu []byte có nghĩa là "một slice của byte".
(Xem Slices: cách dùng và
cơ chế nội bộ để biết thêm về slice.)
Phần tử Body là []byte thay vì
string vì đó là kiểu mà các thư viện io chúng ta sẽ dùng mong
đợi, như bạn sẽ thấy bên dưới.
Struct Page mô tả cách dữ liệu trang sẽ được lưu trong bộ nhớ. Nhưng còn
việc lưu trữ lâu dài thì sao? Chúng ta có thể giải quyết điều đó bằng cách tạo phương
thức save trên Page:
func (p *Page) save() error {
filename := p.Title + ".txt"
return os.WriteFile(filename, p.Body, 0600)
}
Chữ ký của phương thức này có nghĩa là: "Đây là phương thức tên save nhận
p, một con trỏ đến Page, làm receiver. Nó không có tham số
và trả về một giá trị kiểu error."
Phương thức này sẽ lưu Body của Page vào một tệp văn bản.
Để đơn giản, chúng ta sẽ dùng Title làm tên tệp.
Phương thức save trả về một giá trị error vì đó là kiểu trả về
của WriteFile (một hàm thư viện chuẩn ghi một slice byte vào tệp). Phương
thức save trả về giá trị error để cho phép ứng dụng xử lý nó nếu có sự cố
xảy ra khi ghi tệp. Nếu mọi việc suôn sẻ, Page.save() sẽ trả về
nil (giá trị không của con trỏ, interface và một số kiểu khác).
Hằng số nguyên dạng bát phân 0600, được truyền làm tham số thứ ba cho
WriteFile, cho biết rằng tệp nên được tạo với quyền đọc-ghi chỉ dành cho
người dùng hiện tại. (Xem trang man Unix open(2) để biết chi tiết.)
Ngoài việc lưu trang, chúng ta cũng muốn tải trang:
func loadPage(title string) *Page {
filename := title + ".txt"
body, _ := os.ReadFile(filename)
return &Page{Title: title, Body: body}
}
Hàm loadPage xây dựng tên tệp từ tham số tiêu đề, đọc nội dung tệp vào một
biến mới body, và trả về một con trỏ đến một Page literal được
xây dựng với các giá trị tiêu đề và nội dung phù hợp.
Các hàm có thể trả về nhiều giá trị. Hàm thư viện chuẩn
os.ReadFile trả về []byte và error.
Trong loadPage, error chưa được xử lý; "định danh rỗng" được biểu diễn
bằng ký hiệu gạch dưới (_) được dùng để bỏ qua giá trị trả về error
(về bản chất là gán giá trị cho không có gì).
Nhưng điều gì xảy ra nếu ReadFile gặp lỗi? Ví dụ, tệp có thể không tồn tại.
Chúng ta không nên bỏ qua các lỗi như vậy. Hãy sửa hàm để trả về *Page và
error.
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
Những hàm gọi hàm này bây giờ có thể kiểm tra tham số thứ hai; nếu nó là
nil thì Page đã được tải thành công. Nếu không, đó sẽ là một
error mà người gọi có thể xử lý (xem
đặc tả ngôn ngữ để biết chi tiết).
Tại thời điểm này chúng ta có một cấu trúc dữ liệu đơn giản và khả năng lưu vào và tải
từ tệp. Hãy viết một hàm main để kiểm tra những gì chúng ta đã viết:
func main() {
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.Body))
}
Sau khi biên dịch và thực thi mã này, một tệp tên TestPage.txt sẽ được tạo,
chứa nội dung của p1. Tệp sau đó sẽ được đọc vào struct p2, và
phần tử Body của nó sẽ được in ra màn hình.
Bạn có thể biên dịch và chạy chương trình như sau:
$ go build wiki.go $ ./wiki This is a sample Page.
(Nếu bạn dùng Windows bạn phải gõ "wiki" mà không có
"./" để chạy chương trình.)
Nhấn vào đây để xem mã chúng ta đã viết cho đến nay.
Giới thiệu gói net/http (xen kẽ)
Đây là một ví dụ đầy đủ về một máy chủ web đơn giản hoạt động được:
//go:build ignore
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Hàm main bắt đầu bằng lời gọi đến
http.HandleFunc, cho gói http biết xử lý tất cả các yêu cầu
đến root web ("/") bằng
handler.
Sau đó nó gọi http.ListenAndServe, chỉ định rằng nó nên lắng nghe trên cổng
8080 trên bất kỳ interface nào (":8080"). (Đừng lo về tham số thứ hai của
nó, nil, lúc này.)
Hàm này sẽ chặn cho đến khi chương trình bị kết thúc.
ListenAndServe luôn trả về một error, vì nó chỉ trả về khi xảy ra lỗi không
mong đợi. Để ghi log error đó chúng ta bọc lời gọi hàm bằng log.Fatal.
Hàm handler có kiểu http.HandlerFunc.
Nó nhận một http.ResponseWriter và một http.Request làm đối số.
Một giá trị http.ResponseWriter tập hợp response của máy chủ HTTP; bằng cách
ghi vào nó, chúng ta gửi dữ liệu đến HTTP client.
http.Request là cấu trúc dữ liệu đại diện cho HTTP request của client.
r.URL.Path là thành phần đường dẫn của URL trong request. Phần
[1:] ở cuối có nghĩa là "tạo một sub-slice của Path từ ký tự
thứ 1 đến hết." Điều này bỏ đi ký tự "/" đứng đầu trong tên đường dẫn.
Nếu bạn chạy chương trình này và truy cập URL:
http://localhost:8080/monkeys
chương trình sẽ hiển thị một trang chứa:
Hi there, I love monkeys!
Dùng net/http để phục vụ các trang wiki
Để dùng gói net/http, nó phải được import:
import (
"fmt"
"os"
"log"
"net/http"
)
Hãy tạo một handler, viewHandler, cho phép người dùng xem một trang wiki.
Nó sẽ xử lý các URL có tiền tố "/view/".
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
Một lần nữa, lưu ý việc dùng _ để bỏ qua giá trị trả về error
từ loadPage. Điều này được thực hiện ở đây để đơn giản và thường được coi là
thực hành không tốt. Chúng ta sẽ xử lý điều này sau.
Đầu tiên, hàm này trích xuất tiêu đề trang từ r.URL.Path, thành phần đường
dẫn của URL trong request. Path được cắt lại bằng
[len("/view/"):] để bỏ thành phần "/view/" đứng đầu của đường
dẫn request. Điều này là vì đường dẫn sẽ luôn bắt đầu bằng "/view/", không
phải là một phần của tiêu đề trang.
Hàm sau đó tải dữ liệu trang, định dạng trang bằng một chuỗi HTML đơn giản, và ghi vào
w, http.ResponseWriter.
Để dùng handler này, chúng ta viết lại hàm main để
khởi tạo http dùng viewHandler để xử lý bất kỳ yêu cầu nào
dưới đường dẫn /view/.
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Nhấn vào đây để xem mã chúng ta đã viết cho đến nay.
Hãy tạo một số dữ liệu trang (dưới dạng test.txt), biên dịch mã và thử phục
vụ một trang wiki.
Mở tệp test.txt trong trình soạn thảo của bạn và lưu chuỗi "Hello world"
(không có dấu ngoặc kép) vào đó.
$ go build wiki.go $ ./wiki
(Nếu bạn dùng Windows bạn phải gõ "wiki" mà không có
"./" để chạy chương trình.)
Với máy chủ web này đang chạy, truy cập http://localhost:8080/view/test
sẽ hiển thị một trang có tiêu đề "test" chứa các từ "Hello world".
Chỉnh sửa trang
Một wiki không phải là wiki nếu không có khả năng chỉnh sửa trang. Hãy tạo hai handler
mới: một tên editHandler để hiển thị biểu mẫu 'chỉnh sửa trang', và handler
kia tên saveHandler để lưu dữ liệu được nhập qua biểu mẫu.
Đầu tiên, chúng ta thêm chúng vào main():
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Hàm editHandler tải trang
(hoặc, nếu nó không tồn tại, tạo một struct Page rỗng),
và hiển thị một biểu mẫu HTML.
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
fmt.Fprintf(w, "<h1>Editing %s</h1>"+
"<form action=\"/save/%s\" method=\"POST\">"+
"<textarea name=\"body\">%s</textarea><br>"+
"<input type=\"submit\" value=\"Save\">"+
"</form>",
p.Title, p.Title, p.Body)
}
Hàm này sẽ hoạt động tốt, nhưng tất cả HTML được hard-code đó khá xấu xí. Tất nhiên, có một cách tốt hơn.
Gói html/template
Gói html/template là một phần của thư viện chuẩn Go. Chúng ta có thể dùng
html/template để giữ HTML trong một tệp riêng biệt, cho phép chúng ta thay
đổi bố cục trang chỉnh sửa mà không cần sửa đổi mã Go bên dưới.
Đầu tiên, chúng ta phải thêm html/template vào danh sách import. Chúng ta
cũng sẽ không dùng fmt nữa, nên phải xóa nó đi.
import (
"html/template"
"os"
"net/http"
)
Hãy tạo một tệp template chứa biểu mẫu HTML.
Mở một tệp mới tên edit.html và thêm các dòng sau:
<h1>Chỉnh sửa {{.Title}}</h1>
<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Lưu"></div>
</form>
Sửa editHandler để dùng template thay vì HTML được hard-code:
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
t, _ := template.ParseFiles("edit.html")
t.Execute(w, p)
}
Hàm template.ParseFiles sẽ đọc nội dung của
edit.html và trả về một *template.Template.
Phương thức t.Execute thực thi template, ghi HTML được tạo ra vào
http.ResponseWriter.
Các định danh có dấu chấm .Title và .Body tham chiếu đến
p.Title và p.Body.
Các chỉ thị template được đặt trong dấu ngoặc nhọn kép.
Lệnh printf "%s" .Body là một lời gọi hàm
xuất ra .Body dưới dạng chuỗi thay vì một luồng byte,
giống như một lời gọi đến fmt.Printf.
Gói html/template giúp đảm bảo rằng chỉ có HTML an toàn và có cấu trúc đúng
được tạo ra bởi các hành động template. Ví dụ, nó tự động escape bất kỳ dấu lớn hơn
(>), thay thế bằng >, để đảm bảo dữ liệu người dùng
không làm hỏng HTML của biểu mẫu.
Vì chúng ta đang làm việc với template, hãy tạo một template cho
viewHandler của chúng ta tên view.html:
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">chỉnh sửa</a>]</p>
<div>{{printf "%s" .Body}}</div>
Sửa viewHandler cho phù hợp:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
t, _ := template.ParseFiles("view.html")
t.Execute(w, p)
}
Lưu ý rằng chúng ta đã dùng hầu như cùng một mã template trong cả hai handler. Hãy loại bỏ sự trùng lặp này bằng cách chuyển mã template vào một hàm riêng:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Execute(w, p)
}
Và sửa các handler để dùng hàm đó:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
Nếu chúng ta comment out việc đăng ký save handler chưa được triển khai trong
main, chúng ta có thể biên dịch và kiểm thử chương trình một lần nữa.
Nhấn vào đây để xem mã chúng ta đã viết cho đến nay.
Xử lý các trang không tồn tại
Điều gì xảy ra nếu bạn truy cập
/view/APageThatDoesntExist? Bạn sẽ thấy một trang chứa HTML. Điều này
là vì nó bỏ qua giá trị error trả về từ loadPage và tiếp tục cố gắng điền
vào template mà không có dữ liệu. Thay vào đó, nếu trang được yêu cầu không tồn tại, nó
nên chuyển hướng client đến trang chỉnh sửa để nội dung có thể được tạo:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
Hàm http.Redirect thêm mã trạng thái HTTP
http.StatusFound (302) và một header Location
vào HTTP response.
Lưu trang
Hàm saveHandler sẽ xử lý việc gửi biểu mẫu trên các trang chỉnh sửa.
Sau khi bỏ comment dòng liên quan trong main, hãy triển khai handler:
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
p.save()
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
Tiêu đề trang (được cung cấp trong URL) và trường duy nhất của biểu mẫu,
Body, được lưu vào một Page mới.
Phương thức save() sau đó được gọi để ghi dữ liệu vào tệp, và client được
chuyển hướng đến trang /view/.
Giá trị được trả về bởi FormValue có kiểu string.
Chúng ta phải chuyển đổi giá trị đó thành []byte trước khi nó phù hợp với
struct Page. Chúng ta dùng []byte(body) để thực hiện việc chuyển
đổi.
Xử lý lỗi
Có một số chỗ trong chương trình của chúng ta nơi các lỗi đang bị bỏ qua. Đây là thực hành không tốt, không ít nhất vì khi xảy ra lỗi chương trình sẽ có hành vi không mong muốn. Giải pháp tốt hơn là xử lý các lỗi và trả về thông báo lỗi cho người dùng. Như vậy nếu có sự cố xảy ra, máy chủ sẽ hoạt động đúng như chúng ta muốn và người dùng có thể được thông báo.
Đầu tiên, hãy xử lý các lỗi trong renderTemplate:
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)
}
}
Hàm http.Error gửi một mã response HTTP được chỉ định
(trong trường hợp này là "Internal Server Error") và thông báo lỗi.
Quyết định đưa điều này vào một hàm riêng đã bắt đầu phát huy tác dụng.
Bây giờ hãy sửa saveHandler:
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
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)
}
Bất kỳ lỗi nào xảy ra trong p.save() sẽ được báo cáo cho người dùng.
Bộ nhớ đệm template
Có một sự kém hiệu quả trong mã này: renderTemplate gọi
ParseFiles mỗi khi một trang được render.
Cách tốt hơn là gọi ParseFiles một lần khi khởi tạo chương trình, phân tích
tất cả các template vào một *Template duy nhất.
Sau đó chúng ta có thể dùng phương thức
ExecuteTemplate
để render một template cụ thể.
Đầu tiên chúng ta tạo một biến toàn cục tên templates và khởi tạo nó bằng
ParseFiles.
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
Hàm template.Must là một wrapper tiện lợi sẽ panic khi được truyền một giá
trị error khác nil, và ngược lại trả về *Template không thay đổi.
Panic là phù hợp ở đây; nếu các template không thể được tải thì điều hợp lý duy nhất là
thoát khỏi chương trình.
Hàm ParseFiles nhận bất kỳ số lượng đối số chuỗi nào để xác định các tệp
template của chúng ta, và phân tích những tệp đó thành các template được đặt tên theo tên
tệp cơ sở. Nếu chúng ta thêm nhiều template hơn vào chương trình, chúng ta sẽ thêm tên
của chúng vào đối số của lời gọi ParseFiles.
Sau đó chúng ta sửa hàm renderTemplate để gọi phương thức
templates.ExecuteTemplate với tên của template phù hợp:
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)
}
}
Lưu ý rằng tên template là tên tệp template, vì vậy chúng ta phải thêm
".html" vào đối số tmpl.
Xác thực
Như bạn có thể đã nhận thấy, chương trình này có một lỗ hổng bảo mật nghiêm trọng: người dùng có thể cung cấp một đường dẫn tùy ý để đọc/ghi trên máy chủ. Để giảm thiểu điều này, chúng ta có thể viết một hàm để xác thực tiêu đề bằng biểu thức chính quy.
Đầu tiên, thêm "regexp" vào danh sách import.
Sau đó chúng ta có thể tạo một biến toàn cục để lưu biểu thức xác thực của chúng ta:
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
Hàm regexp.MustCompile sẽ phân tích và biên dịch biểu thức chính quy, và
trả về một regexp.Regexp.
MustCompile khác với Compile ở chỗ nó sẽ panic nếu biên dịch
biểu thức thất bại, trong khi Compile trả về một error làm
tham số thứ hai.
Bây giờ, hãy viết một hàm dùng biểu thức validPath
để xác thực đường dẫn và trích xuất tiêu đề trang:
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 // The title is the second subexpression.
}
Nếu tiêu đề hợp lệ, nó sẽ được trả về cùng với giá trị error nil.
Nếu tiêu đề không hợp lệ, hàm sẽ ghi một lỗi "404 Not Found" vào kết nối HTTP và trả về
một error cho handler. Để tạo một error mới, chúng ta phải import gói errors.
Hãy đặt một lời gọi đến getTitle trong mỗi handler:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
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)
}
Giới thiệu hàm literal và closure
Việc bắt điều kiện lỗi trong mỗi handler tạo ra nhiều mã lặp lại. Điều gì sẽ xảy ra nếu chúng ta có thể bọc mỗi handler trong một hàm thực hiện xác thực và kiểm tra lỗi này? Các hàm literal của Go cung cấp một phương tiện mạnh mẽ để trừu tượng hóa chức năng có thể giúp ích ở đây.
Đầu tiên, chúng ta viết lại định nghĩa hàm của mỗi handler để chấp nhận một chuỗi tiêu đề:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) func editHandler(w http.ResponseWriter, r *http.Request, title string) func saveHandler(w http.ResponseWriter, r *http.Request, title string)
Bây giờ hãy định nghĩa một hàm wrapper nhận một hàm có kiểu như trên và trả về
một hàm có kiểu http.HandlerFunc
(phù hợp để truyền cho hàm http.HandleFunc):
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Here we will extract the page title from the Request,
// and call the provided handler 'fn'
}
}
Hàm được trả về được gọi là closure vì nó đóng gói các giá trị được định nghĩa bên ngoài
nó. Trong trường hợp này, biến fn (đối số duy nhất của
makeHandler) được đóng gói bởi closure. Biến
fn sẽ là một trong các handler save, edit hoặc view của chúng ta.
Bây giờ chúng ta có thể lấy mã từ getTitle và dùng ở đây
(với một số sửa đổi nhỏ):
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])
}
}
Closure được trả về bởi makeHandler là một hàm nhận
http.ResponseWriter và http.Request (nói cách khác, một
http.HandlerFunc).
Closure trích xuất title từ đường dẫn request và xác thực nó bằng regexp
validPath. Nếu
title không hợp lệ, một error sẽ được ghi vào
ResponseWriter dùng hàm http.NotFound.
Nếu title hợp lệ, hàm handler đóng gói
fn sẽ được gọi với ResponseWriter,
Request và title làm đối số.
Bây giờ chúng ta có thể bọc các hàm handler bằng makeHandler trong
main, trước khi chúng được đăng ký với gói http:
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
Cuối cùng chúng ta xóa các lời gọi đến getTitle khỏi các hàm handler, làm
cho chúng đơn giản hơn nhiều:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(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)
}
Thử xem nào!
Nhấn vào đây để xem danh sách mã cuối cùng.
Biên dịch lại mã và chạy ứng dụng:
$ go build wiki.go $ ./wiki
Truy cập http://localhost:8080/view/ANewPage sẽ hiển thị biểu mẫu chỉnh sửa trang. Sau đó bạn có thể nhập một số văn bản, nhấn 'Lưu', và được chuyển hướng đến trang mới tạo.
Các bài tập khác
Dưới đây là một số bài tập đơn giản bạn có thể muốn tự thực hiện:
- Lưu trữ template trong
tmpl/và dữ liệu trang trongdata/. - Thêm một handler để làm cho root web chuyển hướng đến
/view/FrontPage. - Làm đẹp các template trang bằng cách làm cho chúng thành HTML hợp lệ và thêm một số quy tắc CSS.
- Triển khai liên kết liên trang bằng cách chuyển đổi các trường hợp của
[PageName]thành
<a href="/view/PageName">PageName</a>. (gợi ý: bạn có thể dùngregexp.ReplaceAllFuncđể làm điều này)