Thủ thuật xử lý lỗi trong Golang

2472

Bài viết được sự cho phép của tác giả Code Toàn Bug

Lỗi là một điều gì đó bất thường xảy ra trong chương trình, nhưng ở Golang lỗi lại mang một ý nghĩa khác. Lỗi chỉ là một giá trị hàm có thể trả về nếu có gì đó không mong đợi xảy ra.

Nếu bạn đã viết mã bằng Golang, ắt hẳn bạn sẽ gặp kiểu error. Go sử dụng các giá trị error để chỉ ra sự bất thường của ứng dụng. Cho ví dụ, hàm os.Open trả về 1 giá trị error khi không mở được tệp:

1 func Open(name string) (file *File, err error)

Đoạn mã sau sử dụng os.Open để mở một tệp. Nếu một lỗi xảy ra, nó sẽ gọi log.Fatal để in thông báo lỗi và dừng lại:

1 f, err := os.Open("filename.ext")
2 if err != nil {
3      log.Fatal(err)
4 }
5 // do something with the open *File f

Bạn có thể làm được nhiều việc với kiểu error trong Go, nhưng trong bài viết này, chúng ta sẽ xem xét kỹ hơn về lỗi và thảo luận một số phương pháp hay để xử lý lỗi trong Go.

  Golang là gì? Top 07 Framework tối ưu “cực căng” cho Golang

Kiểu error

Kiểu error thực chất là 1 kiểu interface. Một biến error đại diện cho bất kỳ giá trị nào có thể mô tả chính nó dưới dạng một string.

Đây là khai báo của interface:

1 type error interface {
2      Error() string
3 }

Với kiểu error, thì Go đã tích hợp sẵn được khai báo trước trong universe block.

Cách triển khai lỗi thường được sử dụng nhất là package errors không hỗ trợ kiểu errorString:

1 // errorString is a trivial implementation of error.
2 type errorString struct {
3      s string
4 }
5
6 func (e *errorString) Error() string {
7      return e.s
8 }

Bạn có thể tạo 1 trong những giá trị này với hàm errors.New. Nó nhận 1 string và convert thành errors.errorString, sau đó chuyển về giá trị error:

1 // New returns an error that formats as the given text.
2 func New(text string) error {
3      return &errorString{text}
4 }

Đây là cách bạn sử dụng errors.New trong hàm tính căn bậc 2:

1 func Sqrt(f float64) (float64, error) {
2      if f < 0 {
3           return 0, errors.New("math: square root of negative number")
4 }
5 // implementation
6 }

Khi người dùng cố tình tính cân bậc 2 của số âm, sẽ có 1 thông báo lỗi đưa ra:

math: square root of negative number.

Chúng ta sử dụng hàm trên như sau:

1 f, err := Sqrt(-1)
2 if err != nil {
3      fmt.Println(err)
4 }

Gói fmt hiển thị error bằng cách gọi phương thức Error() string của nó.

Trách nhiệm của thông báo lỗi là phải mô tả đầy đủ ngữ cảnh của nó. Hàm os.Open trả về lỗi là “open /etc/passwd: permission denied,” chứ không mỗi “permission denied.”  Lỗi do hàm Sqrt của chúng ta trả về là thiếu thông tin về đối số không hợp lệ.

Để thêm thông tin chúng ta thêm gói fmt.Errorf. Nó định dạng một chuỗi theo các quy tắc của Printf và trả về nó dưới dạng một lỗi tạo bởi error.New:

1 if f < 0 {
2      return 0, fmt.Errorf("math: square root of negative number %g", f)
3 }

Trong nhiều trường hợp thì fmt.Errorf là đủ tốt, nhưng vì error là 1 interface, nên bạn có thể chỉnh sửa tùy ý data của bạn như các giá trị error, để bạn kiểm tra tùy theo ngữ cảnh của bạn.

  Channel trong Golang là gì? So sánh Callback function và mutex lock với channel

Cho ví dụ, bạn muốn khôi phục giá trị không hợp lệ của đối số truyền vào Sqrt, bạn có thể định nghĩa 1 kiểu mới thay vì dùng string:

1 type NegativeSqrtError float64
2
3 func (f NegativeSqrtError) Error() string {
4      return fmt.Sprintf("math: square root of negative number %g", float64(f))
5 }

Sau đó chúng ta có thể sử dụng type assertion để kiểm tra NegativeSqrtError và xử lý 1 cách đặc biệt, mà các hàm fmt.Println or log.Fatal sẽ không nhìn thấy sự thay đổi đó.

Một ví dụ khác, package  json chỉ định loại SyntaxError mà hàm json.Decode sẽ trả về lỗi khi không parsing được JSON:

1 type SyntaxError struct {
2      msg string // description of error
3      Offset int64 // error occurred after reading Offset bytes
4 }
5
6 func (e *SyntaxError) Error() string { return e.msg }

Trường Offset sẽ không được hiển thị ở format của error, nhưng chúng ta có thể sử dụng biến line để thêm thông tin về lỗi đó:

1 if err := dec.Decode(&val); err != nil {
2      if serr, ok := err.(*json.SyntaxError); ok {
3          line, col := findLine(f, serr.Offset)
4           return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
5      }
6      return err
7 }

Đoạn code trên thuộc dự án Camlistore.

Interface error chỉ có 1 method Error, triển khai lỗi cụ thể có thể có các methods bổ sung. Cho ví dụ, package net  trả về lỗi thuộc kiểu error, tuân theo quy ước thông thường, nhưng một số triển khai error có các methods bổ sung được xác định bởi interface net.Error:

1 package net
2
3 type Error interface {
4      error
5      Timeout() bool // Is the error a timeout?
6      Temporary() bool // Is the error temporary?
7 }
dser

Client có thể test với net.Error bằng cách kiểm tra các assertion, sau đó phân biệt lỗi mạng tạm thời với lỗi vĩnh viễn. Cho ví dụ, trình crawler web có thể sleep và thử lại khi gặp 1 lỗi tạm thời và từ bỏ nếu gặp lỗi vĩnh viễn:

1 if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
2      time.Sleep(1e9)
3      continue
4 }
5 if err != nil {
6      log.Fatal(err)
7 }

Việc làm Golang Hồ Chí Minh hấp dẫn tại TopDev!

Đơn giản hóa việc xử lý lỗi lặp lại

Trong Go xử lý lỗi là rất quan trọng. Thiết kế và quy ước của ngôn ngữ khuyến khích bạn kiểm tra nơi xảy ra lỗi,(khác biệt với quy ước trong các ngôn ngữ khác là ném ra các ngoại lệ và đôi khi bắt chúng). Trong một số trường hợp, điều này làm cho mã Go trở nên dài dòng, nhưng may mắn thay, bạn có thể sử dụng một số kỹ thuật để giảm thiểu việc xử lý lỗi lặp đi lặp lại.

Hãy xem xét ứng dụng App Engine , với xử lý HTTP lấy các bản ghi từ datastore và định dạng nó bằng 1 template:

1 func init() {
2      http.HandleFunc("/view", viewRecord)
3 }
4
5 func viewRecord(w http.ResponseWriter, r *http.Request) {
6      c := appengine.NewContext(r)
7      key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
8      record := new(Record)
9      if err := datastore.Get(c, key, record); err != nil {
10          http.Error(w, err.Error(), 500)
11           return
12      }
13      if err := viewTemplate.Execute(w, record); err != nil {
14          http.Error(w, err.Error(), 500)
15      }
16 }

Hàm xử lý lỗi trả về bởi hàm datastore.Get, và hàm viewTemplate’s Execute. Trong 2 trường hợp, nó đều trả về 1 lỗi đơn giản với thông báo mã 500 (“Internal Server Error”). Bạn nhận ra là code đã lặp lại.

Để giảm sự lặp lại, bạn định nghĩa 1 kiểu HTTP là appHandler như sau:

1 type appHandler func(http.ResponseWriter, *http.Request) error

Sau đó chúng ta có thể thay đổi hàm viewRecord như sau:

1 func viewRecord(w http.ResponseWriter, r *http.Request) error {
2      c := appengine.NewContext(r)
3      key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
4      record := new(Record)
5      if err := datastore.Get(c, key, record); err != nil {
6           return err
7      }
8      return viewTemplate.Execute(w, record)
9 }

Điều này trông đơn giản hơn, nhưng package http  không hiểu kiểu trả về error. Để khắc phục, chúng ta triển khai interface ServeHTTP của http.Handler như sau:

1 func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2      if err := fn(w, r); err != nil {
3          http.Error(w, err.Error(), 500)
4      }
5 }

Phương thức ServeHTTP gọi hàm appHandler và trả về error nếu có tới user.

Bây giờ chúng ta đăng ký hàm Handle( thay vì hàm HandleFunc) như appHandler là 1 http.Handler(nó không phải là 1 http.HandlerFunc):

1 func init() {
2      http.Handle("/view", appHandler(viewRecord))
3 }

Với cách xử lý này, chúng ta làm nó thân thiện hơn với user. Để hiển thị string thông báo lỗi và HTTP error code, chúng ta tạo 1 struct appError như sau:

1 type appError struct {
2      Error error
3      Message string
4      Code int
5 }

Tiếp theo chúng ta sửa giá trị trả về thuộc kiểu *appError:

1 type appHandler func(http.ResponseWriter, *http.Request) *appError

Và làm cho method appHandler’s ServeHTTP hiển thị thông báo lỗi appError’ chính xác với error code và message cho developer:

1 func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2      if e := fn(w, r); e != nil { // e is *appError, not os.Error.
3          c := appengine.NewContext(r)
4          c.Errorf("%v", e.Error)
5      http.Error(w, e.Message, e.Code)
6      }
7 }

Cuối cùng chúng ta cập nhật lại hàm viewRecord để trả về chi tiết hơn khi gặp lỗi:

1 func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
2      c := appengine.NewContext(r)
3      key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
4      record := new(Record)
5      if err := datastore.Get(c, key, record); err != nil {
6           return &appError{err, "Record not found", 404}
7      }
8      if err := viewTemplate.Execute(w, record); err != nil {
9           return &appError{err, "Can't display record", 500}
10      }
11      return nil
12 }

Phiên bản này có cùng độ dài với phiên bản ban đầu, nhưng mỗi dòng đều có ý nghĩa riêng và thân thiện hơn với người dùng. Nếu bạn muốn cải thiện thì sau đây là 1 số ý tưởng:

  1. Cung cấp cho trình xử lý error bằng 1 template HTML.
  2. Giúp việc debug dễ dàng hơn bằng stack trace vào HTTP response nếu user đó là quản trị viên.
  3. Viết 1 constructor function vào appError để lưu trữ  stack trace cho việc debug dễ dàng hơn.
  4. recover panic bên trong appHandler, logging trên console bằng “Critical”, và thông báo cho user “a serious error has occurred.” Đây là một cách tốt để tránh cho người dùng thấy các thông báo lỗi khó hiểu do lỗi lập trình gây ra. Xem thêm Defer, Panic, and Recover.

Kết luận

Xử lý lỗi là 1 phần quan trọng trong việc viết 1 phần mềm tốt. Bằng việc sử dụng các kỹ thuật trong bài đăng này sẽ giúp bạn viết code Go 1 cách ngắn gọn và đáng tin cậy hơn.

Bạn có thể tham khảo những nhiệm vụ và yêu cầu của lập trình viên Golang thông qua hàng loạt việc làm Golang trên TopDev.

Bài viết gốc được đăng tải tại codetoanbug.com

Xem thêm: