Một trong những chủ đề hấp dẫn và làm đau đầu các Gopher nhập môn có lẽ chính là Interface trong Golang. Interface là một khái niệm không mới trong các ngôn ngữ lập trình phổ biến. Nhưng trong Go thì có cứ lạ làm sau ấy!
Liệu bạn đã dùng Golang Interface đúng cách và hiệu quả? Bài viết này 200Lab sẽ hướng dẫn chi tiết để các bạn dễ dàng sử dụng trong các service Golang nhé.
Định nghĩa Interface trong Golang
An interface type is defined as a set of method signatures. A value of interface type can hold any value that implements those methods - Nguyên văn từ đây.
Interface trong Golang là một kiểu được định nghĩa bởi tập hợp của các method (hàm trong Golang). Interface có thể chứa bất kỳ giá trị gì miễn là nó có implement các method này.
Đoạn code trên có thể được diễn giải rằng: Bất kỳ kiểu dữ liệu nào miễn là có method Speak() string
(không tham số và trả về string
) thì đều được gọi là Speaker
.
Cách sử dụng Interface trong Golang
Implemented ngầm định
Interface trong Golang được implement một cách ngầm định (implicitly), nghĩa là không cần khai báo tường minh (explicitly) bằng cách từ khoá như "implements
" hoặc dấu ":
".
Ví dụ với interface Speaker
ở trên, chúng ta sẽ khai báo một struct bất kỳ để implement Speaker như sau:
Đặc tính này khiến Interface trong Go rất dễ dùng nhưng lại cũng rắc rối cho các bạn mới, đặc biệt những bạn mới từ các ngôn ngữ hỗ trợ mạnh hướng đối tượng (OOP). Mình sẽ nói rõ hơn vấn đề này ở phần sau của bài viết.
Empty Interface thay thế cho mọi kiểu dữ liệu
Hầu hết những người mới tìm hiểu ngôn ngữ Golang đều thắc mắc liệu có kiểu dữ liệu any
để đại diện cho bất kỳ giá trị nào không?! Dù Golang không có keyword any
tuy nhiên các bạn có thể dùng một Interface rỗng (hay Empty Interface) để làm được điều tương đương. Cú pháp: interface{}
:
Trong ví dụ trên biến i
là một interface{}
. Vì interface này không hề định nghĩa và yêu cầu bất kỳ một method nào thành ra nó được dùng cho mọi kiểu. Cũng rất dễ hiểu và đảm bảo nguyên tắc implement ngầm định ở trên.
Một ứng dụng rất phổ biến với interface{}
là sử dụng làm value cho kiểu dữ liệu map
của Golang.
Như vậy ta sẽ có một kiểu map
có key luôn là string
và value là bất kỳ kiểu dữ liệu nào cũng được.
Ép kiểu dữ liệu Interface
Đôi lúc chúng ta sẽ có nhu cầu truy xuất về kiểu dữ liệu gốc để thực hiện các nghiệp vụ tương ứng. Điều này có thể thực hiện thông qua cơ chế Type assertions
trong Golang:
Type Assertions
trong Golang có thể vừa dùng để đưa interface về kiểu dữ liệu bên dưới nó (hoặc một Interface khác nếu thoả điều kiện) vừa dùng để check xem liệu đúng là kiểu dữ liệu muốn ép về hay không. Vì thế, nó có thể trả về một cặp kết quả theo thứ tự: kiểu dữ liệu, giá trị true/false
thể hiện cho việc ép kiểu thành công hoặc thất bại.
Trong trường hợp bạn luôn dùng 2 biến trả về trong Type Assertions thì chương trình vẫn thực thi bình thường bất kể có thành công hay không. Tuy nhiên nếu chỉ dùng một biến sẽ bị panic ngay nếu thất bại.
Những vấn đề thường gặp trong Interface Golang và cách giải quyết
Thường sẽ có 2 vấn đề mà các Gopher thường gặp:
Nhầm lẫn giữa tham trị và tham chiếu (value type và pointer type)
Khi chúng ta khai báo method trên value type và pointer type, các method đó sẽ được gọi tường minh như bình thường. Nhưng với Interface thì sẽ có sự khác biệt:
Các bạn thực thi code trên sẽ báo lỗi: "Bar does not implement Speaker (Speak method has pointer receiver)"
Trong ví dụ trên, struct Foo
implement Speaker với một receiver là value type
. Nhưng struct Bar
lại implement Speaker với một receiver là pointer type
. Kết quả là biến someSpeaker
(có kiểu Speaker Interface) nhận được cho cả value và pointer cho Foo
nhưng lại chỉ dùng với pointer Bar
mà thôi.
Interface nên được khai báo ở tại nơi sử dụng nó
Vấn đề này, 200Lab thấy ở nhiều bạn đến từ các ngôn ngữ OOP khác. Ở các ngôn ngữ này các bạn sẽ thường khai báo các Interfaces trong một package/folder nào đó rồi thực hiệm import để implement. Thói quen này sẽ ảnh hưởng không nhỏ và có thể dẫn đến lỗi cycle import
.
Vấn đề:
Giả sử ở package a các bạn khai báo một Interface:
package a
type Speaker interface {
Speak() string
}
Sau đó ở package b các bạn muốn dùng Interface này thì sẽ phải import A:
package b
import "a"
func someFunc(spk a.Speaker) {
spk.Speak()
}
Thật ra khi chạy chương trình các bạn sẽ không gặp vấn đề gì cả. Nhưng nếu sau này package a có nhu cầu import package b thì chắc chắn không thể compile được do dính lỗi cycle import
(các pakage import lẫn nhau).
Giải pháp
Để làm việc này tốt hơn, chúng ta chỉ cần thay đổi thói quen lại một tí: khai báo interface ngay tại nơi dùng nó. Việc này giúp code không cần import các package khác chỉ để lấy khai báo Interface và code sẽ dễ đọc hơn rất nhiều.
Dù Speaker
ở package a
giống y chang package b
thì các bạn cũng đừng quan tâm nhé. Hãy nhớ rằng Golang không cần implement tường minh, chỉ cần có method Speak() string
thì đã là Speaker
ở cả 2 package.
Các hàm/method (có thể) không nên trả về tường minh Interface
Ngoài ra các bạn có thể không cần phải return tường minh một Interface nếu không thực sự cần thiết.
package b
import "a"
type foo struct{} // mình cố tình private foo luôn nha
func (*foo) Speak() string {
return "Hello, I am Foo"
}
// return tường minh Speaker Interface
func NewFoo() a.Speaker {
return &foo{}
}
Như đoạn code trên các bạn sẽ thấy vì trả về tường minh Interface Speaker nên buộc phải import package có định nghĩa Speaker.
Các bạn có thể trả về như thế này nhé:
package b
type foo struct{}
func (*foo) Speak() string {
return "Hello, I am Foo"
}
func NewFoo() *foo {
return &foo{}
}
Và ở chỗ nào cần dùng đến Speaker
, hay chính xác hơn là type nào có Speak() string
, thì sẽ code như sau:
package main
import (
"b"
"fmt"
)
type Speaker interface {
Speak() string
}
func demoSpeaker(spk Speaker) {
fmt.Println(spk.Speak())
}
func main() {
demoSpeaker(b.NewFoo()) // will be OK
}
Lời kết
Hy vọng qua bài viết trên, 200Lab đã giúp các bạn hiểu được Interface Golang và cách dùng tốt hơn nhé. 200Lab sẽ có những demo Interface thực tế trong các service Golang: cho kiến trúc và Unit Test.
Nếu các bạn thực sự cảm thấy khó khăn trong việc tự học. Thậm chí đã làm được những service cơ bản nhưng vẫn chưa tự tin cho những phần nâng cao thì có thể tham khảo khoá học Golang for Scalable Backend tại 200Lab nhé!
Bài viết cùng chủ đề:
- Clean Architecture là gì - Ưu nhược và cách dùng hợp lý
- Ứng dụng Clean Architecture cho service Golang REST API
- Lập trình REST API TODO List với Golang - Từ UI tới triển khai
Việt Trần
Yêu thích tìm hiểu các công nghệ cốt lõi, kỹ thuật lập trình và thích chia sẻ chúng tới cộng đồng
follow me :
Bài viết liên quan
Design Pattern là gì? 23 Classic Design Pattern với Golang
Aug 21, 2024 • 3 min read
Concurrency pattern trong Go - Phần 2
Apr 11, 2024 • 7 min read
Concurrency pattern trong Golang - Phần 1
Mar 25, 2024 • 8 min read
So sánh Golang và NodeJS chi tiết
Oct 14, 2023 • 22 min read
Buffered Channel là gì? Thường xuyên hỏi trong phỏng vấn Golang Dev
Jun 14, 2023 • 6 min read
Golang Channel là gì? Các ứng dụng Channel trong Golang
Jun 14, 2023 • 12 min read