+1

[Go] Làm một trang web đơn giản dùng Go(Phần 3)

Xin chào các bạn đã quay trở lại với phần 3 của chuyên mục giới thiệu cơ bản về ngôn ngữ Golang của mình. Tại phần này mình xin giới thiệu các xử lý lỗi, validation và dùng template với Go.

1: Xử lý lỗi

Có một số nơi trong chương trình của chúng ta, các lỗi đang bị bỏ qua. Đây là một thực tế không tốt vì khi xảy ra lỗi, chương trình sẽ có hành vi không đúng như mong đợi. Qua đó, có một giải pháp tốt hơn là để xử lý các lỗi và trả lại một thông báo lỗi cho người dùng. Bằng cách đó khi một hành động sai, máy chủ sẽ hoạt động chính xác như chúng ta muốn và người dùng có thể được thông báo cụ thể. Trước tiên, ta 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 một mã phản hồi HTTP được chỉ định (trong trường hợp này là "Internal Server Error") và thông báo lỗi.

Giờ hãy sửa 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)
}

Giờ đây bất kỳ lỗi nào xảy ra trong quá trình p.save() sẽ được báo cáo cho người dùng.

2: Sử dụng mẫu (template)

Có một sự không hiệu quả trong đoạn mã này: renderTemplate gọi ParseFiles mỗi khi một trang được hiển thị. Cách tiếp cận 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 cú pháp tất cả các mẫu thành một *Template. Sau đó chúng ta có thể sử dụng phương thức ExecuteTemplate để hiển thị một mẫu cụ thể.

Đầu tiên chúng ta tạo ra một biến toàn cục có tên là templates, và khởi tạo nó bằng ParseFiles.

var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

Hàm template. Chỉ cần là một khi vượt qua một giá trị lỗi không phải là nil, và nếu không, trả về *Template không thay đổi. Một panics là thích hợp ở đây; nếu các templates không thể được tải điều duy nhất cần làm là thoát khỏi chương trình.

Hàm ParseFiles lấy bất kỳ số đối số chuỗi xác định file template của chúng ta và phân tích các file đó thành các template được đặt tên theo base file name. Nếu chúng ta thêm nhiều mẫu vào chương trình của chúng ta, chúng ta sẽ thêm tên vào đối số của ParseFiles.

Chúng ta sau đó sửa đổi các renderTemplate chức năng để gọi các phương pháp templates.ExecuteTemplate với file template thích hợp:

Lưu ý rằng tên template là tên file template, vì vậy chúng ta phải nối ".html" vào đối số tmpl.

3: Validation

Như bạn đã biết, 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 nhẹ điều này, chúng ta có thể viết một hàm để xác nhận tiêu đề bằng một 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 ra một biến toàn cục để lưu trữ biểu thức xác nhận 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 cú pháp và biên dịch biểu thức chính quy và trả về một regexp.Regexp. MustCompile khác biệt với Compile rằng nó sẽ panic nếu việc biên dịch biểu thức không thành công, trong khi Compile trả về lỗi như một tham số thứ hai.

Bây giờ, hãy viết một hàm sử dụng biểu thức validPath để xác nhận đườ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 đề là hợp lệ, nó sẽ được trả lại cùng với một giá trị không lỗi. Nếu tiêu đề không hợp lệ, chức năng sẽ ghi lỗi "404 Not Found" vào kết nối HTTP và trả lại lỗi cho trình xử lý. Để tạo một lỗi mới, chúng ta phải nhập gói lỗi.

Hãy đặt vào hàm gọi tới getTitle trong mỗi bộ xử lý:

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)
}

4: Giới thiệu chức năng Literals và Closures

Bắt các điều kiện lỗi trong mỗi trình xử lý giới thiệu có rất nhiều mã lặp đi lặp lại. Điều gì sẽ xảy ra nếu chúng ta có thể gói từng bộ xử lý vào một hàm để kiểm tra xác nhận và kiểm tra lỗi? Các chữ cái chức năng của Go cung cấp một phương tiện mạnh mẽ để tóm tắt các chức năng có thể giúp chúng ta ở đây.

Trước tiên, chúng ta viết lại định nghĩa hàm của mỗi trình xử lý để 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ờ chúng ta hãy định nghĩa một hàm wrapper có một hàm của 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 trả về được gọi là đóng bởi vì nó bao gồm 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 bằng cách closure. Biến fn sẽ là một trong những trình lưu trữ, chỉnh sửa hoặc xem bộ điều khiển của chúng ta.

Bây giờ chúng ta có thể lấy code từ getTitle và sử dụng nó ở đâ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 trả lại bởi makeHandler là một chức năng có một http.ResponseWriterhttp.Request (nói cách khác, một http.HandlerFunc). Closure sẽ chiết xuất tiêu đề từ đường dẫn yêu cầu và xác nhận tính hợp lệ với TitleValidator regexp. Nếu tiêu đề không hợp lệ, một lỗi sẽ được ghi vào ResponseWriter sử dụng chức năng http.NotFound. Nếu tiêu đề là hợp lệ, hàm xử lý kèm theo fn sẽ được gọi với ResponseWriter, Request, và title là các đối số.

Bây giờ chúng ta có thể gói các hàm xử lý với 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 loại bỏ các func call tới getTitle từ các chức năng điều khiển, 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)
}

5: Kết luận

Trên đây là tất cả nhũng gì cơ bản nhất về Go, nếu có gì không đúng mong các bạn chỉ ra để mình rút kinh nghiệm. Mình xin cảm ơn các bạn đã xem series của mình.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí