[Golang cơ bản] Interface trong Golang

Post on: 2023-03-16 00:11:12 | in: Golang
Trong Golang, Interface là một kiểu dữ liệu trừu tượng định nghĩa một tập hợp các phương thức mà một đối tượng phải triển khai để trở thành một phần của giao diện đó.

Tổng quan

Interface là một kiểu dữ liệu trong Go được tạo ra bằng cách tập hợp các phương thức có chữ ký cụ thể. Những tập hợp phương thức này được thiết kế để đại diện cho một số hành vi nhất định. Interface chỉ khai báo phương thức và bất kỳ kiểu dữ liệu nào thực hiện tất cả các phương thức của interface đó đều được xem là kiểu dữ liệu của interface đó.

Interface cho phép sử dụng duck typing trong Golang. Vậy duck typing là gì?

Duck typing là một cách tiếp cận trong lập trình máy tính, cho phép thực hiện kiểm tra đối tượng theo cách kiểm tra vịt (duck test) trong đó ta không kiểm tra kiểu dữ liệu mà chỉ kiểm tra sự có mặt của một số thuộc tính hoặc phương thức. Vì vậy, điều quan trọng là đối tượng có các thuộc tính và phương thức cần thiết cho một nhiệm vụ cụ thể hay không, không phải là kiểu dữ liệu của đối tượng đó.

Trong lập trình, điều này có nghĩa là nếu một đối tượng có những thuộc tính và phương thức cần thiết cho một tác vụ cụ thể, thì nó có thể được coi là một phiên bản của một kiểu dữ liệu cụ thể, ngay cả khi nó không được định nghĩa rõ ràng như vậy. Nói cách khác, hành vi và khả năng của đối tượng quan trọng hơn kiểu hoặc lớp của nó.

Trong các ngôn ngữ hỗ trợ duck typing, các đối tượng được xem như là một loại dữ liệu nếu chúng có chữ ký của các phương thức tương tự như một loại dữ liệu nào đó đã được định nghĩa trước đó, ngay cả khi chúng không thuộc kiểu dữ liệu đó.

If it walks like a duck and quack like a duck then it must be duck

Quay lại với interface một lần nữa. Vậy interface là gì? Như đã đề cập trước đó, đó là một tập hợp các chữ ký phương thức. Nó xác định tập chính xác các phương thức mà một kiểu dữ liệu có thể có. Dưới đây là chữ ký của một interface, nó chỉ có chữ ký phương thức.

type name_of_interface interface{
//Method signature 1
//Method signature 2
}

Hãy hiểu khái niệm này với sự giúp đỡ của một ví dụ. Điều đó sẽ rõ hơn. Hãy định nghĩa một interface có tên là animal. Interface animal có hai phương thức breathe và walk. Nó chỉ định chữ ký phương thức và không có gì khác.

type animal interface {
    breathe()
    walk()
}

Một chữ ký phương thức sẽ bao gồm:

  • Tên của phương thức
  • Số lượng đối số và kiểu của mỗi đối số
  • Số lượng giá trị trả về và kiểu của mỗi giá trị trả về

Với khai báo trên, chúng ta đã tạo ra một kiểu interface mới với tên animal. Chúng ta có thể định nghĩa một biến có kiểu animal.

Hãy tạo ra một biến có kiểu interface animal.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

func main() {
    var a animal
    fmt.Println(a)
}

Output

nil

Như đã thấy trong chương trình trên, ta có thể tạo ra một biến có kiểu interface. Khi in ra giá trị của biến đó, nó sẽ hiển thị giá trị mặc định của kiểu interface là nil.

Thực hiện một Interface

Bất kỳ kiểu dữ liệu nào cài đặt được phương thức breathe và walk thì được xem là cài đặt được interface animal. Vì vậy, nếu ta định nghĩa một cấu trúc lion và cài đặt các phương thức breathe và walk thì nó sẽ cài đặt được interface animal.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
    age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

func main() {
    var a animal
    a = lion{age: 10}
    a.breathe()
    a.walk()
}

Output

Lion breathes
Lion walk

Chúng ta khai báo một biến có kiểu giao diện animal.

var a animal

Sau đó, chúng ta gán một instance của cấu trúc lion vào biến đó.

a = lion{}

Việc gán một thể hiện của cấu trúc lion vào một biến có kiểu giao diện animal hoạt động bởi vì cấu trúc lion triển khai cả hai phương thức breathe và walk của animal. Kiểu không được kiểm tra trong quá trình gán giá trị này, thay vào đó, ta chỉ cần kiểm tra kiểu được gán có triển khai được phương thức breathe và walk hay không. Khái niệm này tương tự với duck typing, một con sư tử có thể hít thở và đi bộ như một con vật và do đó nó là một con vật.

Nếu bạn chú ý, bạn sẽ thấy không có khai báo rõ ràng nào cho biết kiểu lion triển khai giao diện animal. Điều này đưa ra một tính chất rất quan trọng liên quan đến interface – "Interface được triển khai một cách ngầm định".

Interface được triển khai một cách ngầm định

Không có khai báo rõ ràng nào cho biết một kiểu triển khai một interface. Trong thực tế, trong Go không tồn tại từ khóa "implements" tương tự như Java. Một kiểu triển khai một interface nếu nó triển khai đầy đủ tất cả các phương thức của interface.

Như đã thấy ở trên, việc định nghĩa một biến có kiểu interface là chính xác và ta có thể gán bất kỳ giá trị kiểu cụ thể nào cho biến này nếu kiểu cụ thể đó triển khai đầy đủ tất cả các phương thức của interface.

Không có khai báo rõ ràng nào cho biết kiểu lion triển khai giao diện animal. Trong quá trình biên dịch, Go nhận ra kiểu lion triển khai tất cả các phương thức của giao diện animal nên nó được phép. Bất kỳ kiểu nào khác triển khai đầy đủ tất cả các phương thức của giao diện animal đều trở thành kiểu của giao diện đó.

Hãy xem một ví dụ phức tạp hơn về một kiểu khác triển khai giao diện animal.

Nếu ta định nghĩa một cấu trúc dog và nó triển khai phương thức breathe và walk thì cũng sẽ là một con vật.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
     age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

type dog struct {
     age int
}

func (l dog) breathe() {
    fmt.Println("Dog breathes")
}

func (l dog) walk() {
    fmt.Println("Dog walk")
}

func main() {
    var a animal

    a = lion{age: 10}
    a.breathe()
    a.walk()

    a = dog{age: 5}
    a.breathe()
    a.walk()
}

Output

Lion breathes
Lion walk
Dog breathes
Dog walk

Cả lion và dog đều triển khai phương thức breathe và walk nên chúng đều có kiểu dữ liệu là animal và có thể được gán cho một biến có kiểu dữ liệu là interface.

Biến interface animal đã được gán giá trị là một instance của lion trước đó và sau đó được gán lại giá trị là một instance của dog. Do đó, kiểu dữ liệu mà biến interface đang tham chiếu đến là động, nó động dẫn đến kiểu dữ liệu của đối tượng gốc.

Có hai điểm quan trọng cần lưu ý:

  • Kiểm tra tĩnh của interface được thực hiện trong quá trình biên dịch - có nghĩa là nếu một kiểu dữ liệu không triển khai tất cả các phương thức của một interface, thì việc gán instance của kiểu dữ liệu đó vào một biến của kiểu interface tương ứng sẽ gây ra lỗi trong quá trình biên dịch. Ví dụ, nếu xoá phương thức walk được định nghĩa trên struct lion, lỗi dưới đây sẽ được thông báo trong quá trình gán giá trị:
cannot use lion literal (type lion) as type animal in assignment:
  • Chương trình sẽ gọi phương thức chính xác dựa trên kiểu của instance tại runtime - có nghĩa là phương thức của lion hoặc dog sẽ được gọi tùy thuộc vào biến interface tham chiếu đến instance của lion hoặc dog. Nếu nó tham chiếu đến một instance của lion, thì phương thức của lion sẽ được gọi và nếu nó tham chiếu đến một instance của dog, thì phương thức của dog sẽ được gọi. Điều này được chứng minh từ kết quả đầu ra. Đây là một cách để đạt được đa hình tại runtime trong Go.

Cần lưu ý rằng các phương thức được định nghĩa bởi kiểu dữ liệu cũng phải khớp với toàn bộ chữ ký của các phương thức trong interface, tức là:

  • Tên của phương thức
  • Số lượng và kiểu của mỗi đối số
  • Số lượng và kiểu của mỗi giá trị trả về

Hãy tưởng tượng rằng interface animal có một phương thức khác là speed trả về giá trị int của tốc độ của động vật.

type animal interface {
    breathe()
    walk()
    speed() int
}

Nếu lion struct có method speed như bên dưới mà không trả về kiểu int, thì lion struct sẽ không thực thi animal interface.

func (l lion) speed()

Nếu struct lion có method speed như sau và không trả về kiểu int, thì struct lion sẽ không implement interface animal.

cannot use lion literal (type lion) as type animal in assignment:
        lion does not implement animal (wrong type for speed method)
                have speed()
                want speed() int

Tóm lại, trong Go, chữ ký của phương thức (method signatures) rất quan trọng khi triển khai (implement) một interface.

Interface types là đối số cho một hàm

Một hàm có thể chấp nhận một đối số của một kiểu interface. Bất kỳ kiểu nào thực hiện giao diện đó đều có thể được truyền vào hàm đó như là đối số. Ví dụ, trong đoạn code dưới đây, chúng ta có hàm callBreathecallWalk nhận đối số của kiểu interface animal. Cả đối tượng liondog đều có thể được truyền vào hàm này. Chúng ta tạo một đối tượng của cả kiểu liondog và truyền chúng vào hàm.

Nó hoạt động tương tự như phép gán chúng ta đã thảo luận ở trên. Trong quá trình biên dịch, không kiểm tra bất kỳ kiểu nào khi gọi hàm, thay vào đó, đối tượng được truyền vào hàm cần đảm bảo thực hiện phương thức breathewalk.

package main

import "fmt"

type animal interface {
breathe()
walk()
}

type lion struct {
     age int
}

func (l lion) breathe() {
fmt.Println("Lion breathes")
}

func (l lion) walk() {
fmt.Println("Lion walk")
}

type dog struct {
     age int
}

func (l dog) breathe() {
fmt.Println("Dog breathes")
}

func (l dog) walk() {
fmt.Println("Dog walk")
}

func main() {
l := lion{age: 10}
callBreathe(l)
callWalk(l)

 d := dog{age: 5}
callBreathe(d)
callWalk(d)
}

func callBreathe(a animal) {
a.breathe()
}

func callWalk(a animal) {
a.breathe()
}

Output

Lion breathes
Lion walk
Dog breathes
Dog walk

Trong đoạn mã trên, chúng ta có hai hàm callBreathe và callWalk nhận đối số là một interface animal. Cả instance của lớp lion và dog đều có thể được truyền vào hàm này. Trong quá trình biên dịch, không có kiểu nào được kiểm tra khi gọi hàm, thay vào đó, chỉ cần kiểm tra rằng kiểu được truyền vào hàm đã implement phương thức breathe và walk là đủ.

Why Interface

Dưới đây là một số lợi ích của việc sử dụng interface.

  • Giúp viết mã modul và phân tách hơn giữa các phần khác nhau của mã nguồn - Nó có thể giúp giảm sự phụ thuộc giữa các phần khác nhau của mã nguồn và cung cấp sự phân tán lỏng lẻo.

Ví dụ, hãy tưởng tượng một ứng dụng tương tác với một lớp cơ sở dữ liệu. Nếu ứng dụng tương tác với cơ sở dữ liệu bằng cách sử dụng giao diện, thì nó sẽ không bao giờ biết về loại cơ sở dữ liệu được sử dụng ở nền tảng. Bạn có thể thay đổi loại cơ sở dữ liệu ở nền tảng, ví dụ như từ ArangoDB sang MongoDB mà không có bất kỳ thay đổi nào ở tầng ứng dụng vì nó tương tác với tầng cơ sở dữ liệu qua một giao diện mà cả ArangoDB và MongoDB đều thực hiện.

  • Interface có thể được sử dụng để đạt được đa hình thời gian chạy trong golang. Đa hình thời gian chạy có nghĩa là cuộc gọi được giải quyết vào thời điểm chạy. Hãy hiểu cách interface có thể được sử dụng để đạt được đa hình thời gian chạy bằng một ví dụ.

Các quốc gia khác nhau có cách tính thuế khác nhau. Điều này có thể được biểu diễn bằng cách sử dụng một interface.

type taxCalculator interface{
    calculateTax()
}

Bây giờ, các quốc gia khác nhau có thể có cấu trúc của riêng họ và có thể thực hiện phương thức "calculateTax()". Cùng một phương thức "calculateTax" được sử dụng trong các ngữ cảnh khác nhau để tính toán thuế. Khi trình biên dịch nhìn thấy cuộc gọi này, nó sẽ trì hoãn việc quyết định chính xác phương thức nào sẽ được gọi vào thời điểm chạy.

package main

import "fmt"

type taxSystem interface {
    calculateTax() int
}
type indianTax struct {
    taxPercentage int
    income        int
}
func (i *indianTax) calculateTax() int {
    tax := i.income * i.taxPercentage / 100
    return tax
}
type singaporeTax struct {
    taxPercentage int
    income        int
}
func (i *singaporeTax) calculateTax() int {
    tax := i.income * i.taxPercentage / 100
    return tax
}
type usaTax struct {
    taxPercentage int
    income        int
}
func (i *usaTax) calculateTax() int {
    tax := i.income * i.taxPercentage / 100
    return tax
}
func main() {
    indianTax := &indianTax{
        taxPercentage: 30,
        income:        1000,
    }
    singaporeTax := &singaporeTax{
        taxPercentage: 10,
        income:        2000,
    }

    taxSystems := []taxSystem{indianTax, singaporeTax}
    totalTax := calculateTotalTax(taxSystems)

    fmt.Printf("Total Tax is %d\n", totalTax)
}
func calculateTotalTax(taxSystems []taxSystem) int {
    totalTax := 0
    for _, t := range taxSystems {
        totalTax += t.calculateTax() //This is where runtime polymorphism happens
    }
    return totalTax
}

Output:

Total Tax is 300

Dưới đây là dòng code mà đa hình thời gian chạy xảy ra.

totalTax += t.calculateTax() //This is where runtime polymorphism happens

Phương thức "calculateTax()" đúng được gọi dựa trên việc thực thể thuộc loại cấu trúc nào, có phải là singaporeTax hay indianTax.

Pointer Receiver trong khi triển khai một interface

Một phương thức của một kiểu có thể có một bộ nhận là con trỏ hoặc giá trị. Trong các ví dụ trên, chúng ta chỉ làm việc với bộ nhận giá trị. Chú ý rằng, bộ nhận con trỏ cũng có thể được sử dụng để thực hiện một giao diện. Nhưng có một lưu ý ở đây:

  • Nếu một kiểu triển khai tất cả các phương thức của một interface bằng bộ nhận giá trị, thì cả biến của kiểu đó và con trỏ đến biến của kiểu đó đều có thể được sử dụng khi gán cho interface đó hoặc khi chuyển đối số vào một hàm mà chấp nhận một đối số là interface đó.
  • Nếu một kiểu triển khai tất cả các phương thức của một interface bằng bộ nhận con trỏ, thì chỉ con trỏ đến biến của kiểu đó có thể được sử dụng khi gán cho interface đó hoặc khi chuyển đối số vào một hàm mà chấp nhận một đối số là interface đó.

Ví dụ để minh họa cho điểm đầu tiên ở trên:

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
    age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

func main() {
    var a animal

    a = lion{age: 10}
    a.breathe()
    a.walk()

    a = &lion{age: 5}
    a.breathe()
    a.walk()
}

Output

Lion breathes
Lion walk
Lion breathes
Lion walk

Các cấu trúc lion triển khai giao diện animal bằng bộ nhận giá trị. Do đó, nó hoạt động cho cả biến của kiểu lion và con trỏ đến biến của kiểu lion.

Điều này hoạt động.

a = lion{age: 10}

cũng như điều này

a = &lion{age: 5}

Ví dụ để minh họa điểm thứ hai trên. Các cấu trúc lion triển khai giao diện animal bằng bộ nhận con trỏ. Do đó, nó chỉ hoạt động cho con trỏ đến biến của kiểu lion.

Vì vậy, điều này hoạt động.

a = &lion{age: 5}

nhưng điều này sẽ gây ra lỗi biên dịch.

a = lion{age: 10}

cannot use lion literal (type lion) as type animal in assignment:
        lion does not implement animal (breathe method has pointer receiver)

Dưới đây là mã hoạt động đầy đủ

package main

import "fmt"

type animal interface {
breathe()
walk()
}

type lion struct {
age int
}

func (l *lion) breathe() {
fmt.Println("Lion breathes")
}

func (l *lion) walk() {
fmt.Println("Lion walk")
}

func main() {
var a animal

 //a = lion{age: 10}
a.breathe()
a.walk()

 a = &lion{age: 5}
a.breathe()
a.walk()
}

Bỏ chú thích trên dòng lệnh đó.

a = lion{age: 10}

và nó sẽ gây ra lỗi biên dịch.

cannot use lion literal (type lion) as type animal in assignment:
        lion does not implement animal (breathe method has pointer receiver)

Triển khai interface với một kiểu tùy chỉnh không phải là cấu trúc

Cho đến nay, chúng ta chỉ đã xem xét các ví dụ của kiểu cấu trúc triển khai interface. Ngoài ra, điều đó hoàn toàn hợp lệ cho bất kỳ kiểu tùy chỉnh không phải là cấu trúc nào để triển khai một interface. Hãy xem một ví dụ.

package main

import "fmt"

type animal interface {
breathe()
walk()
}

type cat string

func (c cat) breathe() {
fmt.Println("Cat breathes")
}

func (c cat) walk() {
fmt.Println("Cat walk")
}

func main() {
var a animal

 a = cat("smokey")
a.breathe()
a.walk()
}

Output

Cat breathes
Cat walk

Chương trình trên minh họa rằng bất kỳ kiểu tùy chỉnh nào cũng có thể triển khai một giao diện. Kiểu cat có kiểu string và triển khai phương thức breathe và walk, do đó, gán một phiên bản của kiểu cat cho một biến kiểu animal là hoàn toàn chính xác.

Triển khai nhiều interfaces

Một kiểu triển khai một interface nếu nó định nghĩa tất cả các phương thức của interface đó. Nếu nó định nghĩa tất cả các phương thức của một interface khác thì nó cũng triển khai interface đó. Về cơ bản, một kiểu có thể triển khai nhiều interface.

Trong chương trình bên dưới, chúng ta có một interface mammal có phương thức feed. Kiểu lion định nghĩa phương thức này, do đó nó triển khai interface mammal.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type mammal interface {
    feed()
}

type lion struct {
     age int
}
func (l lion) breathe() {
    fmt.Println("Lion breathes")
}
func (l lion) walk() {
    fmt.Println("Lion walk")
}
func (l lion) feed() {
    fmt.Println("Lion feeds young")
}
func main() {
    var a animal
    l := lion{}
    a = l
    a.breathe()
    a.walk()
    var m mammal
    m = l
    m.feed()
}

Output

Lion breathes
Lion walk
Lion feeds young

Giá trị mặc định của một interface

Default hoặc giá trị mặc định của một interface là nil. Chương trình dưới đây minh họa điều đó.

package main

import "fmt"
type animal interface {
    breathe()
    walk()
}

func main() {
    var a animal
    fmt.Println(a)
}

Output

nil

Cách thức hoạt động bên trong của một interface

Tương tự như bất kỳ biến nào khác, một biến interface được biểu diễn bởi một kiểu và giá trị. Giá trị của interface, trong khi đó, bao gồm hai bộ giá trị nhỏ hơn

  • Kiểu cơ bản (underlying type)
  • Giá trị cơ bản (underlying value)

Hình dưới đây minh họa cho những điều chúng ta đã đề cập:

Ví dụ, trong trường hợp cấu trúc lion thực thi interface animal sẽ được thực hiện như sau

Golang cung cấp các định danh định dạng để in ra kiểu dữ liệu cơ bản và giá trị cơ bản được biểu diễn bởi giá trị interface.

  • %T có thể được sử dụng để in ra kiểu cụ thể của giá trị interface.
  • %v có thể được sử dụng để in ra giá trị cụ thể của giá trị interface.
package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
    age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

func main() {
    var a animal
    a = lion{age: 10}
    fmt.Printf("Underlying Type: %T\n", a)
    fmt.Printf("Underlying Value: %v\n", a)
}

Output

Concrete Type: main.lion
Concrete Value: {10}

Một interface có thể được nhúng trong một interface khác cũng như có thể được nhúng trong một cấu trúc. Hãy xem từng trường hợp một.

Nhúng một interface vào một interface khác

Một interface có thể được nhúng vào một interface khác cũng như có thể được nhúng vào một cấu trúc. Hãy xem từng trường hợp một.

Nhúng interface vào interface khác

"Một interface có thể nhúng bất kỳ số lượng interface nào vào đó và cũng có thể được nhúng vào bất kỳ interface nào khác. Tất cả các phương thức của các interface nhúng trở thành một phần của interface chứa nó. Đó là một cách để tạo ra một interface mới bằng cách kết hợp một số interface nhỏ. Hãy hiểu điều này qua ví dụ sau đây

Giả sử chúng ta có một interface animal như sau

type animal interface {
    breathe()
    walk()
}

Giả sử rằng có một interface khác có tên là human nhúng interface animal vào trong nó

type human interface {
    animal
    speak()
}

Vì vậy, nếu bất kỳ kiểu nào cần thực thi interface human, thì nó phải định nghĩa các phương thức

  • breathe() và walk() của interface animal (vì animal được nhúng trong human)
  • phương thức speak() của interface human
package main

import "fmt"

type animal interface {
breathe()
walk()
}

type human interface {
animal
speak()
}

type employee struct {
name string
}

func (e employee) breathe() {
fmt.Println("Employee breathes")
}

func (e employee) walk() {
fmt.Println("Employee walk")
}

func (e employee) speak() {
fmt.Println("Employee speaks")
}

func main() {
var h human

 h = employee{name: "John"}
h.breathe()
h.walk()
h.speak()
}

Output

Employee breathes
Employee walk
Employee speaks

Ví dụ khác, interface ReaderWriter trong golang package io (https://golang.org/pkg/io/#ReadWriter) nhúng hai interface khác:

type ReadWriter interface {
    Reader
    Writer
}

Một interface có thể được nhúng trong một struct.

Một interface cũng có thể được nhúng trong một cấu trúc. Tất cả các phương thức của interface được nhúng có thể được gọi thông qua cấu trúc đó. Cách gọi các phương thức này sẽ phụ thuộc vào việc interface được nhúng là một trường đã đặt tên hay một trường ẩn/không tên.

  • Nếu interface được nhúng là một trường đã đặt tên, thì các phương thức của interface phải được gọi thông qua tên của interface đã đặt.
  • Nếu interface được nhúng là một trường ẩn/không tên, thì các phương thức của interface có thể được tham chiếu trực tiếp hoặc thông qua tên của interface.

Hãy xem một chương trình minh họa cho những điểm trên:"

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type dog struct {
    age int
}

func (d dog) breathe() {
    fmt.Println("Dog breathes")
}

func (d dog) walk() {
    fmt.Println("Dog walk")
}

type pet1 struct {
    a    animal
    name string
}

type pet2 struct {
    animal
    name string
}

func main() {
    d := dog{age: 5}
    p1 := pet1{name: "Milo", a: d}

    fmt.Println(p1.name)
    // p1.breathe()
    // p1.walk()
    p1.a.breathe()
    p1.a.walk()

    p2 := pet2{name: "Oscar", animal: d}
    fmt.Println(p1.name)
    p2.breathe()
    p2.walk()
    p1.a.breathe()
    p1.a.walk()
}

Output

Milo
Dog breathes
Dod walk


Oscar
Dog breathes
Dog walk
Dog breathes
Dog walk

Chúng ta đã khai báo hai struct là pet1 và pet2. Struct pet1 có chứa trường interface đã đặt tên là animal.

type pet1 struct {
    a    animal
    name string
}

Struct pet2 có chứa trường interface ẩn/không tên là animal được nhúng vào.

type pet2 struct {
    animal
    name string
}

Đối với một thể hiện của struct pet1, chúng ta gọi phương thức breathe() và walk() như sau:

p1.a.breathe()
p1.a.walk()

Việc gọi trực tiếp các phương thức này sẽ gây ra lỗi biên dịch.

p1.breathe()
p1.walk()

p1.breathe undefined (type pet1 has no field or method breathe)
p1.walk undefined (type pet1 has no field or method walk)

Đối với một thể hiện của struct pet2, chúng ta có thể gọi trực tiếp phương thức breathe() và walk() như sau:

p2.breathe()
p2.walk()

Chúng ta có thể truy cập trực tiếp các phương thức của interface nhúng nếu interface đó là ẩn danh hoặc không có tên.

Dưới đây cũng là một cách hợp lệ khác để gọi các phương thức của interface nhúng ẩn danh/không tên.

p2.animal.breathe()
p2.animal.walk()

Lưu ý rằng trong khi tạo một thể hiện của cấu trúc pet1 hoặc pet2, interface nhúng tức là animal được khởi tạo với một kiểu triển khai của nó, nghĩa là dog.

p1 := pet1{name: "Milo", a: d}
p2 := pet2{name: "Oscar", animal: d}

Nếu chúng ta không khởi tạo giao diện nhúng animal, thì nó sẽ được khởi tạo với giá trị mặc định của giao diện là nil. Gọi phương thức breathe() và walk() trên một thể hiện của cấu trúc pet1 hoặc pet2 như vậy sẽ tạo ra một panic.

Truy cập biến cơ sở của Interface

Biến cơ sở của interface có thể được truy cập bằng hai cách:

  • Phát biểu kiểu (Type Assertion)
  • Chuyển đổi kiểu (Type Switch)

Type Assertion

Type Assertion cung cấp một cách để truy cập biến cốt lõi bên trong giá trị của interface bằng cách xác định kiểu đúng của giá trị cốt lõi. Dưới đây là cú pháp để làm điều đó với i là một interface.

val := i.({type})

Câu lệnh trên khẳng định kiểu dữ liệu của giá trị bên trong của interface là {type}. Nếu khẳng định đúng thì giá trị bên trong sẽ được gán vào val. Nếu không đúng thì câu lệnh trên sẽ gây ra lỗi panic.

package main

import "fmt"

type animal interface {
breathe()
walk()
}

type lion struct {
age int
}

func (l lion) breathe() {
fmt.Println("Lion breathes")
}

func (l lion) walk() {
fmt.Println("Lion walk")
}

type dog struct {
age int
}

func (d dog) breathe() {
fmt.Println("Dog breathes")
}

func (d dog) walk() {
fmt.Println("Dog walk")
}

func main() {
var a animal

 a = lion{age: 10}
print(a)
}

func print(a animal) {
l := a.(lion)
fmt.Printf("Age: %d\n", l.age)

 //d := a.(dog)
//fmt.Printf("Age: %d\n", d.age)
}

Output

Age: 10

Đây là cách chúng ta khai báo biến a kiểu animal có kiểu dữ liệu cơ bản là lion.

l := a.(lion)

Dòng dưới sẽ tạo ra lỗi panic vì kiểu dữ liệu cơ bản là lion chứ không phải dog. Hãy bỏ dấu chú thích trên dòng đó để kiểm tra.

//d := a.(dog)

Kiểu khai báo thông qua type assertion cung cấp một cách khác để lấy giá trị cơ bản và cũng ngăn ngừa lỗi panic. Cú pháp cho điều đó là:

val, ok := i.()

Trong trường hợp này, kiểu khai báo trả về hai giá trị, giá trị đầu tiên giống như đã thảo luận ở trên, giá trị thứ hai là một giá trị boolean chỉ ra liệu kiểu khai báo đó chính xác hay không. Giá trị này là:

  • true nếu kiểu khai báo đúng nghĩa là kiểu được khai báo giống với kiểu cơ bản.
  • false nếu kiểu khai báo thất bại.

Vì vậy, kiểu khai báo thứ hai là một cách tốt để tránh lỗi panic. Hãy xem một ví dụ.

package main

import "fmt"

type animal interface {
breathe()
walk()
}

type lion struct {
age int
}

func (l lion) breathe() {
fmt.Println("Lion breathes")
}

func (l lion) walk() {
fmt.Println("Lion walk")
}

type dog struct {
age int
}

func (d dog) breathe() {
fmt.Println("Dog breathes")
}

func (d dog) walk() {
fmt.Println("Dog walk")
}

func main() {
var a animal

 a = lion{age: 10}
print(a)
}

func print(a animal) {
l, ok := a.(lion)
if ok {
  fmt.Println(l)
} else {
  fmt.Println("a is not of type lion")
}

 d, ok := a.(dog)
if ok {
  fmt.Println(d)
} else {
  fmt.Println("a is not of type lion")
}
}

Output:

{10}
a is not of type lion

Chúng ta tiếp tục sang chuyển đổi kiểu dữ liệu bây giờ.

Type Switch

Chuyển đổi kiểu dữ liệu (Type Switch) cho phép chúng ta thực hiện phát biểu kiểu dữ liệu theo chuỗi. Xem ví dụ mã sau đây để làm điều đó.

package main

import "fmt"

type animal interface {
breathe()
walk()
}

type lion struct {
age int
}

func (l lion) breathe() {
fmt.Println("Lion breathes")
}

func (l lion) walk() {
fmt.Println("Lion walk")
}

type dog struct {
age int
}

func (d dog) breathe() {
fmt.Println("Dog breathes")
}

func (d dog) walk() {
fmt.Println("Dog walk")
}

func main() {
var a animal

 x = lion{age: 10}
print(x)

}

func print(a animal) {
switch v := a.(type) {
case lion:
  fmt.Println("Type: lion")
case dog:
  fmt.Println("Type: dog")
default:
  fmt.Printf("Unknown Type %T", v)
}
}

Output:

Type: lion

Trong đoạn mã trên, chúng ta sử dụng chuyển đổi kiểu dữ liệu để xác định kiểu giá trị chứa trong biến giao diện "x" có phải là "lion", "dog" hoặc một kiểu khác không. Nếu muốn, chúng ta cũng có thể thêm nhiều kiểu dữ liệu khác nhau trong câu lệnh "case".

Empty interface

Một empty interface không có phương thức nào, do đó mặc định tất cả các kiểu cụ thể đều triển khai empty interface. Nếu bạn viết một hàm chấp nhận empty interface thì bạn có thể truyền bất kỳ kiểu dữ liệu nào cho hàm đó. Xem mã hoạt động bên dưới.

package main

import "fmt"

func main() {
    test("thisisstring")
    test("10")
    test(true)
}

func test(a interface{}) {
    fmt.Printf("(%v, %T)\n", a, a)
}

Output

(thisisstring, string)
(10, string)
(true, bool)

Tổng kết

Đó là tất cả những điều về interface trong Go. Hy vọng bạn thích bài viết này. Vui lòng chia sẻ phản hồi / cải tiến / lỗi trong phần bình luận.

 
 
 
 
Tag: golang cơ bản go