Giới thiệu
Những nội dung có trong bài hướng dẫn này:
- Tạo một cấu trúc dữ liệu kèm các method tải và lưu
- Dùng package
net/httpđể xây dựng ứng dụng web - Dùng package
html/templateđể xử lý template HTML - Dùng package
regexpđể kiểm tra dữ liệu người dùng nhập vào - Dùng closure
Kiến thức giả định bạn đã 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 chút kiến thức về dòng lệnh UNIX/DOS
Bắt đầu
Hiện tại, bạn cần một 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 bài hướng dẫn này bên trong GOPATH của bạn rồi cd vào đó:
$ mkdir gowiki
$ cd gowiki
Tạo một tệp tên là wiki.go, mở nó bằng trình soạn thảo ưa thích của bạn, và thêm các dòng sau:
package main
import (
"fmt"
"os"
)
Chúng ta import package fmt và os từ thư viện chuẩn của Go. Về sau, khi hiện thực thêm các tính năng khác, chúng ta sẽ thêm nhiều package nữa vào khai báo import này.
Cấu trúc dữ liệu
Hãy bắt đầu bằng việc định nghĩa các cấu trúc dữ liệu. Một wiki gồm một loạt trang liên kết với nhau, mỗi trang có một tiêu đề và một phần thân (nội dung của trang). Ở đây, chúng ta định nghĩa Page là một struct với hai trường biểu diễn tiêu đề và phần thân.
type Page struct {
Title string
Body []byte
}
Kiểu []byte nghĩa là “một slice các byte”. (Xem Slices: usage and internals để biết thêm về slice.) Phần tử Body có kiểu []byte chứ không phải string, vì đó là kiểu mà các thư viện io chúng ta sắp dùng mong đợi, như bạn sẽ thấy bên dưới.
Struct Page mô tả cách dữ liệu của trang sẽ được lưu trong bộ nhớ. Nhưng còn lưu trữ lâu dài thì sao? Chúng ta có thể giải quyết việc đó bằng cách tạo một method save trên Page:
func (p *Page) save() error {
filename := p.Title + ".txt"
return os.WriteFile(filename, p.Body, 0600)
}
Chữ ký của method này đọc là: “Đây là một method tên save, nhận receiver là p, một pointer tới Page. Nó không nhận tham số nào, và trả về một giá trị kiểu error.”
Method này sẽ lưu Body của Page vào một tệp văn bản. Cho đơn giản, chúng ta sẽ dùng Title làm tên tệp.
Method save trả về một giá trị error vì đó là kiểu trả về của WriteFile (một hàm trong thư viện chuẩn ghi một slice byte vào một tệp). Method save trả về giá trị lỗi đó để ứng dụng tự xử lý nếu có điều gì trục trặc trong lúc ghi tệp. Nếu mọi thứ suôn sẻ, Page.save() sẽ trả về nil (giá trị zero của pointer, interface và một vài kiểu khác).
Hằng số nguyên hệ bát phân 0600, truyền làm tham số thứ ba cho WriteFile, cho biết tệp phải đượ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 các trang, chúng ta cũng sẽ muốn tải các trang lên:
func loadPage(title string) *Page {
filename := title + ".txt"
body, _ := os.ReadFile(filename)
return &Page{Title: title, Body: body}
}
Hàm loadPage dựng tên tệp từ tham số tiêu đề, đọc nội dung của tệp vào một biến mới tên body, rồi trả về một pointer tới một literal Page được dựng với các giá trị tiêu đề và thân phù hợp.
Hàm trong Go có thể trả về nhiều giá trị. Hàm os.ReadFile của thư viện chuẩn trả về []byte và error. Trong loadPage, lỗi chưa được xử lý; “định danh trống” biểu diễn bằng ký hiệu gạch dưới (_) được dùng để vứt bỏ giá trị lỗi trả về (về bản chất là gán giá trị đó cho hư không).
Nhưng chuyện gì xảy ra nếu ReadFile gặp lỗi? Chẳng hạn, tệp có thể không tồn tại. Chúng ta không nên bỏ qua những lỗi như vậy. Hãy sửa hàm này để nó 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
}
Bên gọi hàm này giờ có thể kiểm tra tham số thứ hai; nếu nó là nil thì hàm đã tải Page thành công. Nếu không, nó sẽ là một error mà bên gọi có thể xử lý (xem đặc tả ngôn ngữ để biết chi tiết).
Đế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 tệp cũng như tải từ tệp. Hãy viết một hàm main để thử những gì vừa 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à chạy đoạn mã này, một tệp tên TestPage.txt sẽ được tạo ra, chứa nội dung của p1. Tệp đó sau đó được đọc vào struct p2, và phần tử Body của nó đượ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 thì phải gõ “wiki” mà không có “./” để chạy chương trình.)
Bấm vào đây để xem đoạn mã chúng ta đã viết đến giờ.
Làm quen với package net/http (một khúc dạo)
Đây là một ví dụ hoàn chỉnh, chạy được, về một web server đơn giản:
//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 mở đầu bằng lời gọi http.HandleFunc, câu lệnh này bảo package http xử lý mọi request tới gốc web ("/") bằng handler.
Sau đó nó gọi http.ListenAndServe, chỉ định rằng nó phải lắng nghe ở cổng 8080 trên mọi giao diện mạng (":8080"). (Đừng bận tâm về tham số thứ hai, nil, lúc này.) Hàm này sẽ chặn lại cho tới khi chương trình kết thúc.
ListenAndServe luôn trả về một lỗi, vì nó chỉ trả về khi có lỗi ngoài dự kiến xảy ra. Để ghi lại lỗi đó, 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 dùng để lắp ráp response của HTTP server; bằng cách ghi vào nó, chúng ta gửi dữ liệu tới HTTP client.
http.Request là một cấu trúc dữ liệu biểu diễn request HTTP của client. r.URL.Path là thành phần đường dẫn của URL trong request. Phần [1:] ở cuối nghĩa là “tạo một slice con của Path từ ký tự thứ 1 tới hết.” Điều này loại bỏ dấu “/” đứng đầu khỏi 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 package 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, hãy để ý việc dùng _ để bỏ qua giá trị error mà loadPage trả về. Ở đây làm vậy cho đơn giản, và nói chung đó bị coi là thói quen xấu. Chúng ta sẽ xử lý chuyện này sau.
Trước hết, hàm này trích tiêu đề trang ra 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ỏ đi thành phần "/view/" đứng đầu đường dẫn của request. Sở dĩ vậy là vì đường dẫn bao giờ cũng bắt đầu bằng "/view/", phần không thuộc tiêu đề của trang.
Sau đó hàm tải dữ liệu của trang, định dạng trang bằng một chuỗi HTML đơn giản, rồi ghi nó vào w, tức 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ý mọi request nằm dưới đường dẫn /view/.
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Bấm vào đây để xem đoạn mã chúng ta đã viết đến giờ.
Hãy tạo ít dữ liệu trang (dưới dạng test.txt), biên dịch mã nguồn của chúng ta, rồi 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 thì phải gõ “wiki” mà không có “./” để chạy chương trình.)
Với web server này đang chạy, khi truy cập http://localhost:8080/view/test bạn sẽ thấy một trang có tiêu đề “test” chứa dòng chữ “Hello world”.
Chỉnh sửa trang
Một wiki mà không chỉnh sửa được trang thì chưa phải là wiki. 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à một tên saveHandler để lưu dữ liệu nhập qua biểu mẫu đó.
Trước 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 lên (hoặc, nếu trang chưa tồn tại thì tạo một struct Page rỗng), rồi 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 chạy tốt, nhưng đống HTML viết cứng trong mã nguồn thì xấu xí. Dĩ nhiên, có một cách hay hơn.
Package html/template
Package 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ữ phần HTML trong một tệp riêng, nhờ đó thay đổi được bố cục trang chỉnh sửa mà không phải sửa mã Go bên dưới.
Trước hết, chúng ta phải thêm html/template vào danh sách import. Chúng ta cũng không dùng fmt nữa, nên phải bỏ 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>Editing {{.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="Save"></div>
</form>
Sửa editHandler để dùng template, thay cho phần HTML viết cứng:
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.
Method t.Execute thực thi template, ghi phần HTML sinh ra vào http.ResponseWriter. Các định danh có dấu chấm .Title và .Body tham chiếu tới p.Title và p.Body.
Các directive của template được đặt trong cặp ngoặc nhọn kép. Lệnh printf "%s" .Body là một lời gọi hàm, xuất .Body dưới dạng chuỗi thay vì một dòng byte, giống hệt một lời gọi fmt.Printf. Package html/template giúp bảo đảm rằng các action của template chỉ sinh ra HTML an toàn và đúng đắn. Chẳng hạn, nó tự động escape mọi dấu lớn hơn (>), thay bằng >, để chắc chắn dữ liệu người dùng không làm hỏng phần HTML của biểu mẫu.
Vì giờ chúng ta đang làm việc với template, hãy tạo một template cho viewHandler, đặt tên là view.html:
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>
Sửa viewHandler cho tương ứng:
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)
}
Hãy để ý rằng chúng ta đã dùng đoạn mã template gần như y hệt nhau ở cả hai handler. Hãy loại bỏ sự trùng lặp này bằng cách chuyển đoạn mã template ra 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 chú thích lại dòng đăng ký handler lưu trang chưa hiện thực trong main, chúng ta có thể build và thử chương trình một lần nữa. Bấm vào đây để xem đoạn mã chúng ta đã viết đến giờ.
Xử lý các trang không tồn tại
Sẽ ra sao nếu bạn truy cập /view/APageThatDoesntExist? Bạn sẽ thấy một trang chứa HTML. Đó là vì chương trình bỏ qua giá trị lỗi trả về từ loadPage và cứ thế điền template với dữ liệu rỗng. Thay vào đó, nếu Page được yêu cầu không tồn tại, nó nên chuyển hướng client tới Page chỉnh sửa để nội dung có thể được tạo ra:
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 response HTTP.
Lưu trang
Hàm saveHandler sẽ xử lý việc gửi đi các biểu mẫu nằm trên trang chỉnh sửa. Sau khi bỏ chú thích dòng liên quan trong main, hãy hiện thực handler này:
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 (lấy từ URL) và trường duy nhất của biểu mẫu, Body, được lưu vào một Page mới. Method save() sau đó được gọi để ghi dữ liệu vào một tệp, và client được chuyển hướng tới trang /view/.
Giá trị mà FormValue trả về có kiểu string. Chúng ta phải chuyển giá trị đó sang []byte thì nó mới vừa 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ó vài chỗ trong chương trình của chúng ta mà lỗi đang bị bỏ qua. Đây là thói quen xấu, mà lý do lớn nhất là khi lỗi thực sự xảy ra, chương trình sẽ hành xử ngoài ý muốn. Giải pháp tốt hơn là xử lý lỗi và trả về một thông báo lỗi cho người dùng. Như vậy nếu có gì trục trặc, server sẽ hoạt động đúng như chúng ta muốn và người dùng được thông báo.
Trước hết, hãy xử lý 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 đi một mã response HTTP được chỉ định (ở đây là “Internal Server Error”) cùng một thông báo lỗi. Quyết định tách phần này ra một hàm riêng đã bắt đầu phát huy tác dụng.
Giờ hãy chỉnh lại 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)
}
Mọi lỗi xảy ra trong lúc p.save() chạy sẽ được báo cho người dùng.
Cache template
Đoạn mã này có một điểm kém hiệu quả: renderTemplate gọi ParseFiles mỗi lần một trang được render. Cách làm tốt hơn là gọi ParseFiles một lần lúc khởi tạo chương trình, phân tích toàn bộ template vào một *Template duy nhất. Sau đó chúng ta có thể dùng method ExecuteTemplate để render một template cụ thể.
Trước hết 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 lớp bọc tiện dụng: nó panic khi được truyền một giá trị error khác nil, còn nếu không thì trả về *Template nguyên vẹn. Panic là phù hợp ở đây; nếu không tải được các template thì điều hợp lý duy nhất có thể làm là thoát chương trình.
Hàm ParseFiles nhận một số lượng tuỳ ý các đối số chuỗi 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 template khác 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 method 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 chính là tên tệp template, nên chúng ta phải nối thêm ".html" vào đối số tmpl.
Kiểm tra hợp lệ
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 tuỳ ý để đọc/ghi trên server. Để giảm nhẹ điều này, chúng ta có thể viết một hàm kiểm tra tiêu đề bằng biểu thức chính quy.
Trước hết, 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 kiểm tra của mình:
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, rồi trả về một regexp.Regexp. MustCompile khác Compile ở chỗ nó sẽ panic nếu việc 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.
Giờ hãy viết một hàm dùng biểu thức validPath để kiểm tra đường dẫn và trích ra 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ề kèm một giá trị lỗi nil. Nếu tiêu đề không hợp lệ, hàm sẽ ghi lỗi “404 Not Found” vào kết nối HTTP, và trả về một lỗi cho handler. Để tạo một lỗi mới, chúng ta phải import package errors.
Hãy đặt một lời gọi getTitle vào từng 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)
}
Làm quen với function literal và closure
Việc bắt điều kiện lỗi trong từng handler sinh ra rất nhiều mã lặp lại. Sẽ thế nào nếu chúng ta có thể bọc mỗi handler trong một hàm lo phần kiểm tra hợp lệ và kiểm tra lỗi này? Function literal của Go là một công cụ mạnh để trừu tượng hoá chức năng, và có thể giúp chúng ta ở đây.
Trước hết, chúng ta viết lại định nghĩa hàm của từng handler để nhận thêm 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)
Giờ hãy định nghĩa một hàm bọc nhận vào một hàm thuộc kiểu ở trên, và trả về một hàm 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ề gọi là một closure vì nó bao bọc các giá trị được định nghĩa bên ngoài nó. Ở đây, biến fn (đối số duy nhất của makeHandler) được closure bao bọc. Biến fn sẽ là một trong các handler lưu, chỉnh sửa hoặc xem trang của chúng ta.
Giờ chúng ta có thể lấy đoạn mã từ getTitle và dùng lại ở đây (với vài chỉnh sửa 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 mà makeHandler trả về là một hàm nhận một http.ResponseWriter và một http.Request (nói cách khác, một http.HandlerFunc). Closure này trích title ra từ đường dẫn của request, và kiểm tra nó bằng regexp validPath. Nếu title không hợp lệ, một lỗi sẽ được ghi vào ResponseWriter bằng hàm http.NotFound. Nếu title hợp lệ, hàm handler được bao bọc là fn sẽ được gọi với các đối số ResponseWriter, Request và title.
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 package 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 bỏ các lời gọi getTitle khỏi các hàm handler, khiến 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ử ngay thôi!
Bấm vào đây để xem toàn bộ mã nguồn cuối cùng.
Biên dịch lại mã nguồn, và chạy ứng dụng:
$ go build wiki.go
$ ./wiki
Khi truy cập http://localhost:8080/view/ANewPage bạn sẽ thấy biểu mẫu chỉnh sửa trang. Khi đó bạn có thể nhập một ít văn bản, bấm ‘Save’, và được chuyển hướng tới trang vừa được tạo.
Các bài tập khác
Sau đây là vài việc đơn giản bạn có thể tự mình thử sức:
- Lưu các template trong
tmpl/và dữ liệu trang trongdata/. - Thêm một handler để gốc web chuyển hướng tới
/view/FrontPage. - Làm đẹp các template của trang bằng cách biến chúng thành HTML hợp lệ và thêm vài quy tắc CSS.
- Hiện thực việc liên kết giữa các trang bằng cách chuyển các chỗ xuất hiện của
[PageName]thành
<a href="/view/PageName">PageName</a>. (gợi ý: bạn có thể dùngregexp.ReplaceAllFuncđể làm việc này)