Writing Web Applications

Giới thiệu

Những nội dung được đề cập trong hướng dẫn này:

Kiến thức được giả định trước:

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 fmtos 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[]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ề []byteerror. 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ề *Pageerror.

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.Body tham chiếu đến p.Titlep.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 &gt;, để đả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.ResponseWriterhttp.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, Requesttitle 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: