Cách sử dụng interfaces trong Golang (Phần 1)

1824

Bài viết được sự cho phép của tác giả Lê Xuân Quỳnh

Để kiến thức của bài học sẽ không trôi đi mất sau khi đọc xong, bạn hãy mở Visual Code hay bất cứ IDE nào có thể code được Golang ra và thực hành.

Giới thiệu về interfaces

Interfaces là gì? Một interface có 2 điểm: Nó là 1 tập hợp các phương thức (methods), nhưng cũng là 1 kiểu. Trước tiên chúng ta hãy tập hợp vào điểm thứ nhất là tập hợp các methods.

Thông thường, chúng ta sẽ giới thiệu về interfaces thông qua các ví dụ cụ thể, dễ hiểu. Hãy xem xét 1 ví dụ thực tế là giả sử chúng ta cần định nghĩa kiểu dữ liệu Động vật (Animal). Kiểu Animal có 1 interface đó là động vật có thể giao tiếp được bằng tiếng. Gà kêu ò ó o, chó sủa gâu gâu, mèo kêu meo meo.. chẳng hạn như vậy.

Đây là khái niệm cốt lõi trong Golang, thay vì thiết kế các loại giao diện trừu tượng mà data có thể chứa, thì chúng ta thiết kế những hành động mà các kiểu của data có thể thực thi.

Hãy bắt đầu bằng interface của lớp Animal như sau:

type Animal interface {
     Speak() string
}

Trông rất đơn giản: động vật có thể “nói” bằng ngôn ngữ của chúng. Phương thức Speak không nhận đối số và trả về 1 kiểu string để viết ra âm thanh mà con vật có thể phát ra(meo meo, gâu gâu…). Bất kỳ kiểu nào định nghĩa phương thức này đều thỏa mãn interface Animal. Không có từ khóa implements trong Go. Việc 1 kiểu có thỏa mãn hay không được xác định tự động. Hãy tạo 1 vài kiểu thỏa mãn giao diện này như sau:

type Dog struct {
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
}

func (c Cat) Speak() string {
    return "Meow!"
}

type Llama struct {
}

func (l Llama) Speak() string {
    return "?????"
}

type JavaProgrammer struct {
}

func (j JavaProgrammer) Speak() string {
    return "Design patterns!"
}

Bây giờ chúng ta có 4 con vật: 1 con chó, 1 mèo, 1 thằng Llama và 1 thằng lập trình viên java(xin lỗi đây chỉ là ví dụ cho vui thôi nha). Trong hàm main chúng ta có thể tạo 1 slices chứa 4 động vật này như sau:

Bạn có thể chạy và xem kết quả tại đây: http://play.golang.org/p/yGTd4MtgD5

Tuyệt vời, bây giờ bạn đã biết cách sử dụng các giao diện và tôi không cần phải nói về chúng nữa, phải không? Không, không hẳn. Hãy xem xét một số điều không quá rõ ràng đối với những con chuột chũi mới chớm :v

Kiểu interface{}

Kiểu interface{} là 1 kiểu interface rỗng, là nguồn gốc của mọi sự nhầm lẫn. Rõ ràng nó không có method nào cả. Vì không có từ khóa nào để triển khai, và do vậy tất cả các kiểu trong Golang đều thỏa mãn nó 1 cách tự động. Điều đó có nghĩa là nếu bạn viết 1 hàm nhận interface{} làm đối số, bạn có thể truyền vào bất kỳ kiểu nào. Cho 1 ví dụ:

func DoSomething(v interface{}) {
   // ...
}

Nó sẽ chấp nhận bất kỳ tham số nào.

Ở đây nó tạo ra 1 sự khó hiểu: vậy v ở trong hàm DoSomething là kiểu dữ liệu gì? Những người mới bắt đầu được dẫn dắt để tin rằng v là bất kỳ kiểu nào, nhưng điều đó là sai. v không thuộc kiểu nào, nó thuộc kiểu interface rỗng. Chờ đã, gì cơ? Khi chuyển một giá trị vào hàm DoSomething, thời gian thực Go sẽ thực hiện chuyển đổi kiểu (nếu cần) và chuyển đổi giá trị đó thành giá trị interface{}. Tất cả giá trị đó là chính xác trong thời gian thực, và nó là 1 kiểu tĩnh(static) của interface{}. Điều này sẽ khiến bạn tự hỏi: được rồi, vậy nếu một sự chuyển đổi đang diễn ra, thì điều gì đang thực sự được chuyển vào một hàm nhận giá trị {} interface (hoặc, thứ thực sự được lưu trữ trong [] Animal slice)? Giá trị của 1 interface được tạo thành bởi 2 điều: Một là con trỏ trỏ tới method table định nghĩa tên biến cho loại kiểu; và một là sử dụng để trỏ đến dữ liệu thực tế đang được giữ bởi giá trị biến đó. Nếu bạn muốn tìm hiểu thêm về cách triển khai các giao diện, tôi nghĩ rằng mô tả của Russ Cox về các giao diện là rất, rất hữu ích.

interfaces trong golang

Trong ví dụ trên của chúng ta, khi chúng ta xây dựng một phần giá trị Animal, chúng ta không cần phải nói điều gì đó khó hiểu như Animal (Dog {}) để đặt một giá trị của kiểu Dog vào slices Animals, bởi vì chuyển đổi đã được xử lý tự động. trong slice Animals, mỗi phần tử thuộc kiểu Animal, nhưng chúng có giá trị khác nhau từ các kiểu khác nhau.

Vậy… tại sao điều này lại quan trọng? Chà, việc hiểu cách các interface được biểu diễn trong bộ nhớ làm cho một số điều có thể gây nhầm lẫn trở nên rất rõ ràng. Cho ví dụ, với câu hỏi “Tôi có thể convert kiểu []T thành kiểu []interface{} không?” rất dễ trả lời khi bạn hiểu cách các giao diện được biểu diễn trong bộ nhớ.

Dưới đây là một số mã gây hiểu lầm phổ biến về interface:

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
}
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    PrintAll(names)
}

Chạy code tại đây: http://play.golang.org/p/4DuBoi2hJU

Khi chạy bạn sẽ nhận được lỗi sau:

cannot use names (type []string) as type []interface {} in argument to PrintAll

Nếu chúng ta thực sự muốn làm cho nó hoạt động, chúng ta sẽ phải chuyển đổi []string thành []interface{}:

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
}
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    vals := make([]interface{}, len(names))
    for i, v := range names {
       vals[i] = v
}
    PrintAll(vals)
}

Chạy code ở đây: http://play.golang.org/p/Dhg1YS6BJS

Điều này khá xấu, nhưng nó chạy  ổn. Không phải mọi thứ đều hoàn hảo. (trong thực tế, điều này không xuất hiện thường xuyên, bởi vì []interface{}hóa ra ít hữu ích hơn như bạn mong đợi ban đầu.

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

Xem thêm:

TopDev có hàng loạt Top IT Jobs đang chờ bạn ứng tuyển!