Giới thiệu về Interface trong Golang
Giới thiệu về Interface
Interface trong Golang là một trong những tính năng tốt và nổi bật nhất của ngôn ngữ Golang, nhưng chúng cũng là một trong những tính năng khó hiểu nhất đối với người mới. Bài viết này sẽ cố gắng cung cấp cho bạn sự hiểu biết cần thiết để sử dụng interface của Go và không cảm thấy thất vọng khi mọi thứ không hoạt động như cách bạn mong đợi. Nó hơi dài nhưng phần lớn trong số đó chỉ là các ví dụ về code.
Interface của Go khác với Interface trong các ngôn ngữ khác, chúng được thực thi một cách ngầm định. Điều này có nghĩa là bạn không cần phải chỉ ra kiểu dữ liệu của bạn đang implement interface nào một cách rõ ràng như trong các ngôn ngữ OOP thuần túy (như class CFoo implement IFoo). Thay vào đó, kiểu dữ liệu của bạn chỉ cần có các method được xác định trong interface và trình biên dịch sẽ tự động hiểu và thực hiện việc ngầm định rằng kiểu dữ liệu của bạn đang implment theo interface đó.
Ví dụ:
type Walker interface {
Walk(miles int)
}
type Camel struct {
Name string
}
func (c Camel) Walk(miles int) {
fmt.Printf(“%s is walking %v miles\n”, c.Name, miles)
}
func LongWalk(w Walker) {
w.Walk(500)
w.Walk(500)
}
func main() {
c := Camel{“Bill”}
LongWalk(c)
}
// prints
// Bill is walking 500 miles.
// Bill is walking 500 miles.
http://play.golang.org/p/erodX-JplO
Camel implement interface Walker vì nó có một method tên là Walk, nó nhận vào một tham số kiểu int và không trả về bất cứ thứ gì. Điều này có nghĩa là bạn có thể truyền nó vào hàm LongWalk, mặc dù bạn chưa bao giờ chỉ định rằng kiểu Camel là Walker. Trên thực tế, Camel và Walker có thể ở trong các package hoàn toàn khác nhau và chẳng cần biết gì về nhau, và nó vẫn sẽ hoạt động bình thường nếu 1 package thứ ba quyết định tạo ra type Camel và truyền nó vào hàm LongWalk.
Non-Standard Continuation
Đây là nơi hầu hết các bài hướng dẫn dừng lại và là nơi bắt đầu của các câu hỏi. Vấn đề là bạn vẫn không biết các interface thực sự hoạt động như thế nào và từ nảy tới giờ bạn vẫn không thấy nó thực sự phức tạp đến thế , nên hãy nói về điều đó.
Điều gì thực sự xảy ra khi bạn truyền Camel vào LongWalk?
Trước hết, bạn không chuyển cả 1 cái type Camel vào LongWalk. Trên thực tế, bạn đang gán c, một giá trị thuộc type Camel cho một giá trị w thuộc type Walker và w là những gì bạn thao tác trong LongWalk. Đúng chứ ?
Dưới vỏ bọc đó, interface Walker nòi riêng (tất cả các interfce nói chung), sẽ trông ít nhiều giống thế này trong Golang (đây chỉ là pseudo code mô tả một cách gần đúng để dễ đọc và hiểu hơn). Khi bạn thấy hơi khó hiểu , hãy đưa mắt xuống một chút tại (*) để quan sát kết quả , đôi khi đi từ kết quả sẽ làm bạn dể hiểu hơn nhiều.
type Walker struct {
type InterfaceType
data *void
}
type InterfaceType struct {
valtype *gotype
func0 *func
func1 *func
...
}
Tất cả các giá trị của interface chỉ là hai con trỏ - một con trỏ trỏ tới thông tin của interface type và một con trỏ trỏ tới dữ liệu từ giá trị bạn đã truyền vào interface.
InterfaceType chứa một con trỏ tới thông tin về type của giá trị mà bạn truyền vào interface (valtype). Nó cũng chứa các con trỏ tới các method có sẵn trên interface.
(*) Khi bạn gán biến c cho biến w, trình biên dịch sẽ tạo ra các instruction gần giống như thế này (nó không thực sự là code Golang, đây chỉ là 1 đoạn code Golang gần đúng để dễ đọc hơn):
data := c -> Biên data copied từ biến c ra một vùng nhớ mới
w := Walker{
type: &InterfaceType{
valtype: &typeof(c),
func0: &Camel.Walk
}
data: &data -> Biến data của interface sẽ reference với địa chỉ cũa data
}
Khi bạn gán giá trị của biến Camel c cho biến Walker w, type Camel được sao chép vào trường InterfaceType.valtype của Walker interface. Dữ liệu thực tế của biến c bây giờ được sao chép vào một vị trí mới trong bộ nhớ và trường data của biến w sẽ trỏ vào vị trí bộ nhớ đó.
Implications of the Implementation
Bây giờ chúng ta hãy xem ý nghĩa của đoạn code trên. Đầu tiên, value mà interface chứa rất nhỏ - chỉ có hai con trỏ. Khi bạn gán một giá trị cho một interface, giá trị đó sẽ được sao chép một lần vào interface, và sau đó, interface đó sẽ lưu trự địa chỉ tới vùng nhớ data được copy đó..
Vì vậy, bây giờ bạn đã biết lý do tại sao bạn không cần truyền con trỏ vào các interface rối chứ - Bởi vì bản thân interface đã copy value ra 1 vùng nhớ khác và lưu lại dưới dạng pointer. Nhưng dù sao thì chúng cũng nhỏ nên bạn không phải lo lắng về vấn đề sao chép bộ nhớ có gây nên việc hao tổn tài nguyên hay không, ngoài ra chúng còn giữ dữ liệu của bạn trong một con trỏ, do đó sự thay đổi dữ liệu cũng sẽ đi liền với sự thay đổi interface
Interface là kiểu dữ liệu
Hãy nhìn lại Walker, điều này rất quan trọng:
Type interface Walker
Lưu ý rằng từ đầu tiên: type. Intrerface là kiểu dữ liệu, giống như string là một type hoặc Camel là một type.
Bây giờ, giả sử bạn có chức năng này:
func LongWalkAll(walkers []Walker) { for _, w := range walkers { LongWalk(w) } }Và giả sử bạn có một đoàn lạc đà mà bạn muốn gửi đi trong một chuyến đi dài:
caravan := []Camel{ Camel{“Bill”}, Camel{“Bob”}, Camel{“Steve”}}Bạn muốn truyền caravan vào LongWalkAll, trình biên dịch có cho phép bạn không? Không. Tại sao vậy?
Chà, []Walker là một type cụ thể, đó là một slice các giá trị của type Walker. Nó không phải là cách viết tắt của “một slice của bất cứ thứ gì implement interface Walker”. Đây thực sự là một type riêng biệt, []string khác với []int thi ở đây[]Walker cũng khác với []Camel. Vì vậy, với các slices, bạn phải tự làm như sau:
walkers := make([]Walker, len(caravan))
for n, c := range caravan {
walkers[n] = c
}
LongWalkAll(walkers)
Tuy nhiên, có một cách tốt hơn
caravan := []Walker{ Camel{“Bill”}, Camel{“Bob”}, Camel{“Steve”}}
LongWalkAll(caravan)
Lưu ý rằng điều này áp dụng cho bất kỳ type nào bao gồm interface như một phần trong định nghĩa của nó: không có sự chuyển đổi tự động từ func(Camel) của bạn thành func(Walker) hoặc map[string]Camel thành map[string]Walker. Một lần nữa, chúng là những type hoàn toàn khác nhau, chúng không phải là viết tắt và chúng không phải là alias.
Điều gì sẽ xảy ra nếu method Camel's Walk có kí hiệu (*) này thay thế?
func (c *Camel) Walk(miles int)Dòng này cho biết type *Camel có method Walk. Điều này rất quan trọng: *Camel là một type. Khác biệt với type Camel. Nên điều này sẽ xảy khi bạn cố gắng gán nó cho một interface.
Lưu ý Camel và *Camel có thể coi 2 type riêng biệt.
c := Camel{“Bill”}
LongWalk(c)
// compiler output:
cannot use c (type Camel) as type Walker in function argument:
Camel does not implement Walker (Walk method has pointer receiver)
Để truyền Camel vào LongWalk bây giờ, bạn cần truyền một con trỏ Camel:
c := &Camel{“Bill”}
LongWalk(c)
or
c := Camel{“Bill”}
LongWalk(&c)
Lưu ý rằng điều này đúng mặc dù bạn vẫn có thể gọi Walk trực tiếp trên Camel:
c := Camel{“Bill”}
c.Walk(500) // this works
Lý do bạn có thể làm điều đó là vì trình biên dịch Go tự động chuyển đổi dòng này thành (&c).Walk(500) cho bạn. Tuy nhiên, điều đó không có tác dụng khi chuyển giá trị vào interface. Lý do là giá trị trong interface nằm ở một vị trí bộ nhớ ẩn và do đó trình biên dịch không thể tự động lấy con trỏ tới bộ nhớ đó cho bạn (theo cách nói của Go, điều này được gọi là “không thể đánh địa chỉ”).
Con trỏ Nil và Giao diện Nil
Sự tương tác giữa interface nil và con trỏ, gần như mọi người đều gặp khó khăn khi bắt đầu với Go.
Giả sử chúng ta có type Camel với method Walk được xác định trên *Camel như trên và chúng ta muốn tạo một function trả về một Walker thực sự là một Camel (lưu ý rằng bạn không cần một hàm để thực hiện việc này, bạn có thể chỉ cần gán *Camel cho Walker, nhưng hàm này là một ví dụ minh họa hay):
func MakeWalker() Walker {
return &Camel{“Bill”}
}
w := MakeWalker()
if w != nil {
w.Walk(500) // we will hit this
}
Điều này hoạt động tốt. Nhưng bây giờ, nếu chúng ta làm điều gì đó khác biệt một chút thì sao:
func MakeWalker(c *Camel) Walker {
return c
}
var c *Camel
w := MakeWalker(c)
if w != nil {
// we’ll get in here, but why?
w.Walk(500)
}
Mã này cũng sẽ nằm trong câu lệnh if (và sau đó là hoảng loạn, chúng ta sẽ nói đến điều này sau) vì giá trị Walker được trả về không bằng 0. Làm sao điều đó có thể xảy ra nếu chúng ta trả về một con trỏ nil? Chà, chúng ta hãy quay lại các hướng dẫn được tạo khi chúng ta gán giá trị cho một giao diện.
data := c
w := Walker{
type: &InterfaceType{
valtype: &typeof(c),
func0: &Camel.Walk
}
data: &data
}
Trong trường hợp này, c là một con trỏ nil. Tuy nhiên, đó là giá trị hoàn toàn hợp lệ để gán cho giá trị Dữ liệu của Walker, vì vậy nó hoạt động tốt. Những gì bạn trả về là một giá trị Walker khác 0, có một con trỏ tới 0 *Camel làm dữ liệu. Vì vậy, tất nhiên, nếu bạn kiểm tra w == nil, câu trả lời là sai, w không phải nil… nhưng sau đó bên trong câu lệnh if, chúng ta thử gọi Camel's walk:
func (c *Camel) Walk(miles int) {
fmt.Printf(“%s is walking %v miles\n”, c.Name, miles)
}
Và khi chúng tôi cố gắng thực hiện c.Name, Go sẽ tự động biến nó thành (*c).Name và mã sẽ gặp lỗi với lỗi quy chiếu con trỏ nil.
Hy vọng rằng điều này có ý nghĩa, dựa trên sự hiểu biết mới của chúng ta về cách các giao diện bao bọc các giá trị, nhưng sau đó bạn tính toán các con trỏ nil như thế nào? Giả sử bạn muốn MakeWalker trả về giao diện nil nếu nó được chuyển qua Camel nil. Bạn phải gán rõ ràng nil cho giao diện:
func MakeWalker(c *Camel) Walker {
if c == nil {
return nil
}
return c
}
var c *Camel
w := MakeWalker(c)
if w != nil {
// Yay, we don’t get here!
w.Walk(500)
}
Và bây giờ, cuối cùng, mã đang thực hiện những gì chúng ta mong đợi. Khi bạn chuyển vào nil *Camel, chúng tôi trả về giao diện nil. Đây là một cách khác để viết hàm:
func MakeWalker(c *Camel) Walker {
var w Walker
if c != nil {
w = c
}
return w
}
Điều này kém tối ưu hơn một chút nhưng nó cho thấy một cách khác để có được giao diện nil, đó là sử dụng giá trị 0 cho giao diện, tức là nil.
Lưu ý rằng bạn có thể có giá trị con trỏ nil thỏa mãn giao diện. Bạn chỉ cần cẩn thận để không hủy đăng ký con trỏ trong các phương thức của mình. Ví dụ: nếu phương thức *Camel's Walk trông như thế này:
func (c *Camel) Walk(miles int) {
fmt.Printf(“I’m walking %d miles!”, miles)
}
Lưu ý rằng phương thức này không hủy bỏ tham chiếu c và do đó bạn có thể gọi nó ngay cả khi c bằng 0:
var c *Camel
c.Walk(500)
// prints “I’m walking 500 miles!”
http://play.golang.org/p/4EfyV21at9
Khác
Tôi hy vọng bài viết này giúp bạn hiểu rõ hơn về cách hoạt động của giao diện và giúp bạn tránh một số cạm bẫy phổ biến và quan niệm sai lầm mà người mới gặp phải về cách hoạt động của giao diện. Nếu bạn muốn biết thêm thông tin về phần bên trong của giao diện và một số cách tối ưu hóa mà tôi chưa đề cập ở đây, hãy đọc bài viết của Russ Cox về giao diện Go, tôi thực sự khuyên bạn nên làm điều đó.