Bài 19 - Một số kiểu interface quan trọng trong Go


  • Trùm cuối

    Interface

    Trong bài trước chúng ta tìm hiểu về interface qua các ví dụ đơn giản để dễ hiểu. Trong bài này chúng ta sẽ tìm hiểu về các kiểu interface quan trọng, hay được sử dụng trong quá trình chúng ta lập trình bằng Go.

    da83cb65-fb20-4ebb-aa98-8908fd4d886d-image.png

    Xử lý lỗi với interface error

    Trong bài 16 chúng ta chỉ biết error là kiểu interface. Bây giờ chúng ta sẽ cùng tìm hiểu chi tiết về nó để xử lý lỗi khi cần. Interface error gồm một phương thức duy nhất là Error() như sau:

    type error interface {
        Error() string
    }
    

    Cách đơn giản nhất để tạo một biến kiểu error là dùng hàm errors.New(<Chuỗi mô tả lỗi>) thuộc package errors. Hàm này sẽ trả về kiểu error với giá trị chuỗi mô tả lỗi. Nếu muốn xuất lỗi ra màn hình, ta có thể dùng fmt.Errorf mà về bản chất là nó gọi lại errors.New():

    func Errorf(format string, args ...interface{}) error {
        return errors.New(Sprintf(format, args...))
    }
    

    Bây giờ chúng ta cùng xem xét việc xử lý lỗi khi đọc, ghi file. Có nhiều nguyên nhân dẫn đến lỗi khi đọc ghi file nhưng có 3 nguyên nhân phổ biến là:

    • File không tồn tại khi đọc file.
    • File đã tồn tại khi tạo file.
    • Không có quyền đọc/ghi file này.

    Package os cung cấp 3 hàm tương ứng giúp kiểm tra 3 nguyên nhân này là IsNotExist(err error), IsExist(err error)IsPermission(err error). Cả 3 hàm đều trả về kiểu bool. Để mô tả rõ hơn về lỗi thì package os tạo ra cấu trúc PathError để lưu thông tin lỗi gồm:

    type PathError struct {
        Op string
        Path string
        Err error
    }
    
    func (e *PathError) Error() string {
        return e.Op + " " + e.Path + ": " + e.Err.Error()
    } 
    

    Với việc khai báo phương thức Error(), PathError thỏa mãn interface error nên biến err trong 3 hàm nêu trên có kiểu dữ liệu cụ thể là PathError.

    Ví dụ trước khi xử lý tác vụ nào đó liên quan đến một file, chúng ta thường kiểm tra xem file tồn tại hay không. Với Go việc đó có thể thực hiện như sau:

    fileinfo, err := os.Stat("<đường dẫn đến file>") 
    if os.IsNotExist(err) { 
        fmt.Println("File không tồn tại!") 
        return 
    } 
    
    // Xử lý file 
    ...
    

    Hàm os.Stat trả về cấu trúc mô tả file nếu đọc thành công, lỗi sẽ trả về *PathError lưu trong biến err. Hàm os.IsNotExist nhận biến err làm tham số. Nếu err chứa thông tin lỗi do không tồn tại thì hàm IsNotExist sẽ trả về true. Cần xác định rõ lỗi thì có thể lấy err.Error().

    Nhập xuất dữ liệu

    Hệ điều hành họ Unix có một tính năng rất tuyệt là đầu ra một chương trình có thể là đầu vào một chương trình khác và khi thực thi tạo một chuỗi liên hoàn rất thú vị nhưng cực kỳ hiệu quả. Lúc này stdin và stdout đóng vai trò như các điểm tiếp nhận phân phối dữ liệu giữa các tiến trình. Package io của Go được tạo dựa trên ý tưởng này và nó hoạt động cũng rất tuyệt.

    Package io hỗ trợ xử lý các dòng dữ liệu rất hữu hiệu bất kể loại dữ liệu là gì và chúng đến từ đâu, sẽ đi đâu. Hai interface io.Readerio.Writer đóng vai trò như là stdin và stdout. Các thực thể kiểu dữ liệu cụ thể thỏa mãn 2 interface này có thể dùng được tất cả các chức năng cung cấp bởi package io và những package khác mà cũng thỏa mãn hai interface này. Lúc này lập trình viên chúng ta có thể tập trung trên các vấn đề logic ứng dụng, các chức năng bên dưới liên quan đọc ghi đã có package io lo với 2 interface Reader và Writer.

    type Writer interface { 
        Write(p []byte) (n int, err error) 
    } 
     
    type Reader interface { 
        Read(p []byte) (n int, err error) 
    } 
    
    • Interface Writer khai báo hàm Write nhận tham số slice kiểu byte và trả về số lượng byte ghi được và biến lỗi. Trong trường hợp n < len(p) thì biến err phải khác nil.
    • Interface Reader khai báo hàm Read có cùng tham số và kiểu trả về như Writer. Go khuyên xử lý số byte đọc được trước khi kiểm tra biến lỗi err.

    Trong ví dụ bên dưới chúng ta cùng xem 3 package khác nhau cùng xử lý dữ liệu:

    var b bytes.Buffer 
    b.Write([]byte("Hello "))
    fmt.Fprintf(&b, "World!")
    b.WriteTo(os.Stdout)
    
    • Dòng đầu tiên khai báo b thuộc kiểu bytes.Buffer. bytes.Buffer là một cấu trúc thuộc package bytes lưu trữ một vùng nhớ các byte dữ liệu có kích thước không cố định. Buffer có khai báo 2 phương thức Read và Write của io.Reader và io.Writer nhưng theo dạng vật nhận là con trỏ:
    func (b *Buffer)Write(p []byte) (n int, err error) 
    func (b *Buffer)Read(p []byte) (n int, err error)
    
    • Dòng tiếp theo sử dụng phương thức Write để ghi một slice byte có nội dung là các ký tự tạo chuỗi "Hello " vào vùng nhớ biến b.
    • Dòng thứ 3, sử dụng hàm fmt.Fprintf để ghi thêm chuỗi "World!" vào vùng nhớ biến b. Tham số đầu của hàm fmt.Fprintf là io.Writer và như ở trên ta thấy là con trỏ b đáp ứng io.Writer. Đó là lý do chúng ta cần dùng &b thay vì b.
    func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
    
    • Dòng cuối cùng dùng phương thức WriteTo của bytes.Buffer mà tham số của nó là io.Writer.
      func (b *Buffer) WriteTo(w io.Writer) (n int64, err error)
      Trong khi đó os.Stdout là biến được tạo từ hàm NewFile và có kiểu dữ liệu là *File, đáp ứng io.Writer.
    var (
        Stdin   = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
        Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
        Stderr  = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
    ) 
    
    func NewFile(fd uintptr, name string) *File
    
    func (f *File) Write(b []byte) (n int, err error)
    

    Kết quả đoạn code trên sẽ xuất ra màn hình chuỗi "Hello World!". Qua ví dụ này bạn có thể thấy sự tuyệt vời mà interface mang lại và sức mạnh của io.Writer và io.Reader.

    Sắp xếp với sort.Interface

    Sắp xếp là một tác vụ rất hay sử dụng và thường chúng ta phải tự cài đặt với một trong các thuật toán sắp xếp như nhị phân, nổi bọt, chèn hay sắp xếp nhanh, v.v.... Go cung cấp package sort giúp chúng ta thực hiện việc sắp xếp với khai báo đơn giản, nhẹ nhàng.

    Package sort cung cấp interface Interface với 3 phương thức để đáp ứng việc sắp xếp các phần tử của một dãy theo thứ tự:

    type Interface interface {
        Len() int
        Less(i, j int) bool
        Swap(i, j int)
    }
    
    • Phương thức đầu tiên xác định chiều dài, là số phần tử của dãy cần sắp xếp.
    • Phương thức thứ 2 mô tả cách thức so sánh, như thế nào thì phần tử i < phần tử j.
    • Phương thức thứ 3 mô tả cách thức hoán đổi vị trí phần tử i và j nếu cần phải hoán đổi.
      Như vậy, mảng, slice hay bất kỳ dãy các phần tử nào chỉ cần khai báo 3 phương thức này của sort.Interface thì chúng ta có thể dùng sort.Sort() để đáp ứng nhu cầu sắp xếp của mình.

    Bây giờ chúng ta cùng thử xem áp dụng sort.Interfacesort.Sort vào việc sắp xếp một slice các chuỗi ký tự như thế nào:

    package main  
    
    import (  
        fmt 
        sort 
    )  
    
    type StringSlice []string  
    
    func (p StringSlice) Len() int { return len(p) }  
    func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }  
    func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }  
    
    func main() {  
        var names = []string{"Mai", "Lan", "Cúc", "Trúc"} 
        sort.Sort(StringSlice(names)) 
        fmt.Println(names) // [Cúc Lan Mai Trúc] 
    }   
    
    • Dòng 8 khai báo kiểu StringSlice là slice các chuỗi cần sắp xếp, ở đây là tên của một nhóm học sinh.
    • Dòng 10-12 khai báo 3 phương thức của StringSlice giúp nó thỏa mãn sort.Interface. Như chúng ta thấy, nội dung phương thức rất đơn giản:
      🤞 Dòng 10 khai báo phương thức Len, và nội dung trả về số phần tử biến p kiểu StringSlice, là slice các chuỗi cần sắp xếp.
      🤞 Dòng 11 khai báo phương thức Less, mô tả cách so sánh giữa 2 phần tử. Ở đây đơn giản chúng ta so sánh trực tiếp các phần tử là các chuỗi với nhau.
      🤞 Dòng 12 khai báo phương thức Swap, mô tả cách hoán đổi vị trí giữa 2 phần tử mà việc cài đặt cũng rất đơn giản bằng cách hoán đổi 2 chuỗi.
    • Dòng 14-18 là khai báo hàm main:
      🤞 Đầu tiên là tạo slice các chuỗi tên cần so sánh là Mai, Lan, Cúc và Trúc ở dòng 15.
      🤞 Dòng 16 gọi hàm sort.Sort từ package sort. Hàm này có tham số duy nhất là interface Interface. Do StringSlice đáp ứng interface Interface nên việc gọi hàm ở dòng 16 là hợp lệ. StringSlice là khai báo lại từ []string nên ép kiểu names về StringSlice là hợp lệ.
      🤞 Dòng 17 cho kết quả xử lý ở dòng 16 như đã thấy là Cúc, Lan, Mai và Trúc. Slice đã được sắp xếp.

    Lưu ý ở đây là phương thức Less tôi mô tả cách thức so sánh là so sánh trực tiếp 2 chuỗi. Cách so sánh này sẽ không cho kết quả chính xác với chuỗi tiếng Việt có dấu nói riêng và chuỗi Unicode nói chung bởi khi so sánh chuỗi, Go sẽ so sánh từng byte tương ứng. Các bạn có thể viết lại phương thức Less để đảm bảo chức năng sắp xếp này chạy đúng với mọi chuỗi Unicode như một thử thách nhé!

    Ở trên là ví dụ sắp xếp tăng dần slice chuỗi tên. Vậy giờ nếu muốn sắp xếp giảm dần thì sao? Lại tạo ra kiểu dữ liệu mới và cài đặt lại 3 phương thức trên à? Không, package sort cung cấp tiếp cho chúng ta hàm Reverse thực hiện việc này:

    func Reverse(data Interface) Interface
    

    nên chúng ta chỉ cần gọi hàm Sort như sau:

    sort.Sort(sort.Reverse(StringSlice(names)))
    

    kết quả lúc này là [Trúc Mai Lan Cúc].

    Thực ra việc khai báo StringSlice ở dòng 8 là không cần thiết bởi package sort đã khai báo nó rồi, chúng ta có thể dùng mà không cần khai báo lại. Ngoài ra package sort còn khai báo cả IntSlice, Float64Slice để giúp chúng ta dễ dàng sử dụng khi cần sắp xếp các slice kiểu int hay float64. Trong package sort cũng có ví dụ về sắp xếp các cấu trúc. Các bạn tham khảo thêm ở đây.

    Tạo web server với http.Handler interface

    Bất kỳ lập trình viên back-end nào cũng đều quan tâm web server vì đó là trung tâm của nhận yêu cầu và xử lý. Go cung cấp phương tiện tạo web server vô cùng đơn giản nhưng mạnh mẽ.

    Để tạo một web server, chúng ta chỉ cần gọi hàm http.ListenAndServe(address string, h Handler) thuộc package net/http với address là địa chỉ và port, ví dụ localhost:8080 và khai báo một kiểu dữ liệu đáp ứng interface Handler để xử lý các yêu cầu từ client:

    type Handler interface { 
        ServeHTTP(w ResponseWriter, r *Request) 
    } 
    func ListenAndServe(address string, h Handler) error
    

    Hàm ListenAndServe một khi được gọi sẽ chạy mãi cho đến khi lỗi hoặc ứng dụng được đóng. Lúc đó giá trị lỗi trả về là khác nil.

    Bây giờ chúng ta cùng khảo sát ví dụ tạo web server trả về tên của sinh viên khi nhập vào mã của sinh viên đó:

    package main 
     
    import ( 
        "fmt" 
        "net/http" 
        "strconv" 
    ) 
    
    type Students map[int]string 
    
    func (s Students) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
        id, _ := strconv.Atoi(r.URL.Query().Get("id")) 
        if len(s[id]) > 0 { 
            fmt.Fprintf(w, "Tên sinh viên có mã %d là: %s\n", id, s[id]) 
        } else { 
            fmt.Fprintf(w, "Không có sinh viên có mã %d!\n", id) 
        } 
    } 
    
    func main() { 
        sv := Students{1: "Mai", 2: "Lan", 3: "Cúc", 4: "Trúc"} 
        http.ListenAndServe("localhost:8080", sv) 
    }
    
    • Dữ liệu lưu trữ thông tin sinh viên được chọn là map[int]string như khai báo ở dòng 9.
    • Dòng 11-18 là khai báo phương thức ServerHTTP của Students. Với khai báo này, thực thể kiểu Students sẽ thỏa mãn interface Handler:
      👻 Dòng 12 lấy thông tin mã sinh viên. Phương thức Query() của thuộc tính URL của http.Request sẽ trả về cho chúng ta giá trị mà client (trình duyệt) gửi đến ứng với tham số "id". Giá trị trả về kiểu string nên chúng ta cần chuyển về int.
      👻 Dòng 13-17 xử lý với giá trị id nhận được. Nếu giá trị id này đúng là khóa của Students thì chúng ta sẽ trả về tên sinh viên như ở dòng 14. Ngược lại chúng ta thông báo là không có sinh viên với mã này.
      👻 Hàm fmt.Fprintf sẽ ghi nội dung vào biến w kiểu http.ResponseWriter, là interface đáp ứng io.Writer. Nội dung sẽ được chuyển về client và hiển thị ở trình duyệt nếu client là trình duyệt.
    • Hàm main được khai báo với 2 dòng lệnh đơn giản:
      👻 Đầu tiên là khai báo biến sv kiểu Students và khởi tạo giá trị cho nó.
      👻 Dòng tiếp theo là gọi hàm http.ListenAndServe với sv là tham số thứ 2 do nó đã khai báo phương thức ServeHTTP ở dòng 11 nên đáp ứng Handler.
    • Sau khi chạy chương trình ở trên, vào trình duyệt gõ http://localhost:8080/?id=1 sẽ cho kết quả là: Tên sinh viên có mã 1 là: Mai, còn nếu gõ http://localhost:8080/?id=0 sẽ nhận được Không có sinh viên nào có mã 0.

    Thực tế web server phục vụ nhiều request với những yêu cầu khác nhau nên việc tạo kiểu dữ liệu đáp ứng Handler như trên là rất phức tạp. Chúng ta sẽ nói rõ hơn phần này ở bài về web server.

    Trong bài tới, chúng ta sẽ tìm hiểu về khái niệm hướng đối tượng trong Go.

    Tóm tắt

    • Interface error có phương thức duy nhất là Error() trả về chuỗi nội dung lỗi. Để tạo thực thể kiểu error, ta thường dùng errors.New(<chuỗi lỗi>). Để mô tả chi tiết lỗi, có thể tạo kiểu dữ liệu lưu trữ thông tin lỗi rồi khai báo phương thức Error() để kiểu dữ liệu này đáp ứng error và dùng nó khi cần xuất lỗi.
    • Package io cung cấp 2 interface Writer và Reader đảm nhận việc ghi và đọc dữ liệu từ các nguồn, các loại dữ liệu khác nhau. Chỉ cần khai báo kiểu dữ liệu mới có phương thức Writer([]byte)Read([]byte) đáp ứng Writer và Reader là chúng ta có thể dễ dàng thực hiện đọc ghi dữ liệu mong muốn.
    • Để sắp xếp một dãy các phần tử theo thứ tự nào đó, chúng ta tạo kiểu dữ liệu chứa dãy dữ liệu cần sắp xếp, khai báo 3 hàm Len(), Less()Swap() để kiểu dữ liệu này đáp ứng sort.Interface rồi gọi sort.Sort() với tham số là thực thể kiểu dữ liệu cần sắp xếp là ta sẽ có dãy đã được sắp xếp. Cần sắp xếp theo thứ tự ngược lại chỉ cần gọi hàm sort.Sort(sort.Reverse(<thực thể cần sắp xếp>)).
    • Để tạo web server, có thể gọi hàm http.ListenAndServe(<địa chỉ:port>, <thực thể đáp ứng Handler>) và khai báo kiểu dữ liệu đáp ứng http.Hanlder để xử lý các yêu cầu từ client.


Có thể bạn cũng quan tâm

.
DMCA.com Protection Status