Interfaces


1. Interfaces là gì?

  • Interface trong Go 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 (method signatures) mà bất kỳ kiểu nào muốn "triển khai" interface đó phải cung cấp.

  • Go sử dụng implicit implementation (triển khai ngầm), nghĩa là một kiểu không cần khai báo rõ ràng rằng nó triển khai một interface, miễn là nó có các phương thức khớp với interface.

Ví dụ cơ bản:

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func main() {
    var s Shape = Circle{Radius: 5}
    fmt.Println(s.Area()) // Output: 78.53981633974483
}
  • Trong ví dụ trên, Circle tự động triển khai Shape vì nó có phương thức Area() khớp với interface.


2. Cú pháp và cách khai báo Interface

  • Interface được khai báo bằng từ khóa typeinterface.

Cú pháp:

type <TênInterface> interface {
    <TênPhươngThức1>(<parameters>) <returnType>
    <TênPhươngThức2>(<parameters>) <returnType>
    // ...
}

Ví dụ với nhiều phương thức:

type Vehicle interface {
    Start() string
    Stop() string
    Speed() float64
}

type Car struct {
    CurrentSpeed float64
}

func (c Car) Start() string {
    return "Car started"
}

func (c Car) Stop() string {
    return "Car stopped"
}

func (c Car) Speed() float64 {
    return c.CurrentSpeed
}

3. Implicit Implementation - Lợi ích và ý nghĩa

  • Không cần khai báo rõ ràng: Không giống Java hay C#, Go không yêu cầu kiểu dữ liệu phải tuyên bố rằng nó triển khai một interface. Điều này giúp mã linh hoạt hơn và giảm sự phụ thuộc.

  • Tính linh hoạt: Một kiểu có thể triển khai nhiều interface mà không cần sửa đổi mã của kiểu đó.

Ví dụ:

type Printable interface {
    Print() string
}

func (c Car) Print() string {
    return fmt.Sprintf("Car with speed: %.2f", c.CurrentSpeed)
}

func main() {
    c := Car{CurrentSpeed: 100}
    var v Vehicle = c
    var p Printable = c
    fmt.Println(v.Start()) // Output: Car started
    fmt.Println(p.Print()) // Output: Car with speed: 100.00
}
  • Car triển khai cả VehiclePrintable mà không cần khai báo gì thêm.


4. Empty Interface (interface{})

  • Empty interface (interface{}) là một interface không có phương thức nào, nghĩa là bất kỳ kiểu dữ liệu nào trong Go cũng triển khai nó.

  • Được sử dụng để lưu trữ giá trị của bất kỳ kiểu nào (tương tự any trong Go 1.18+).

Ví dụ:

func printAnything(v interface{}) {
    fmt.Println(v)
}

func main() {
    printAnything(42)          // Output: 42
    printAnything("Hello")     // Output: Hello
    printAnything(Circle{5})   // Output: {5}
}

Lưu ý:

  • Sử dụng interface{} quá nhiều có thể làm mã khó đọc và mất tính an toàn kiểu (type safety).

  • Thay vào đó, hãy cân nhắc sử dụng type assertion hoặc generics (Go 1.18+) để tăng độ rõ ràng.


5. Type Assertion và Type Switch

Khi làm việc với interface{}, bạn thường cần kiểm tra hoặc ép kiểu để truy cập vào kiểu dữ liệu cụ thể.

5.1. Type Assertion

func process(v interface{}) {
    if s, ok := v.(string); ok {
        fmt.Println("String:", s)
    } else if i, ok := v.(int); ok {
        fmt.Println("Int:", i)
    } else {
        fmt.Println("Unknown type")
    }
}

func main() {
    process("Hello") // Output: String: Hello
    process(42)      // Output: Int: 42
    process(3.14)    // Output: Unknown type
}

5.2. Type Switch

Type switch là cách ngắn gọn hơn để xử lý nhiều kiểu:

func process(v interface{}) {
    switch val := v.(type) {
    case string:
        fmt.Println("String:", val)
    case int:
        fmt.Println("Int:", val)
    default:
        fmt.Println("Unknown type")
    }
}

Ứng dụng thực tế: Type assertion và type switch thường được dùng khi xử lý JSON không xác định cấu trúc hoặc khi làm việc với các giao thức như gRPC, HTTP.


6. Interfaces và Composition

  • Go khuyến khích composition thay vì kế thừa. Interface là công cụ mạnh mẽ để kết hợp các hành vi từ nhiều kiểu khác nhau.

Ví dụ:

type Reader interface {
    Read() string
}

type Writer interface {
    Write(string)
}

type ReadWriter interface {
    Reader
    Writer
}

type File struct {
    Content string
}

func (f *File) Read() string {
    return f.Content
}

func (f *File) Write(s string) {
    f.Content = s
}

func main() {
    f := &File{Content: "Initial"}
    var rw ReadWriter = f
    fmt.Println(rw.Read()) // Output: Initial
    rw.Write("Updated")
    fmt.Println(rw.Read()) // Output: Updated
}
  • ReadWriter kết hợp cả ReaderWriter, và File triển khai nó một cách tự nhiên.


7. Interfaces và Nil

  • Một interface trong Go bao gồm (type, value). Một interface chỉ là nil khi cả type và value đều là nil.

Ví dụ:

var i interface{}
fmt.Println(i == nil) // Output: true

var s *string
i = s
fmt.Println(i == nil) // Output: false (type là *string, value là nil)

Lưu ý:

  • Khi làm việc với interface, kiểm tra nil không đủ. Hãy kết hợp type assertion hoặc reflection để xử lý chính xác.


8. Interfaces và Error Handling

  • Interface error là một trong những interface nổi tiếng nhất trong Go:

type error interface {
    Error() string
}

Ví dụ tùy chỉnh error:

type MyError struct {
    Code    int
    Message string
}

func (e MyError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func doSomething() error {
    return MyError{Code: 500, Message: "Something went wrong"}
}

func main() {
    if err := doSomething(); err != nil {
        fmt.Println(err) // Output: Error 500: Something went wrong
    }
}

Mẹo:

  • Sử dụng errors.Aserrors.Is (từ Go 1.13) để kiểm tra và unwrap error:

if myErr, ok := err.(MyError); ok {
    fmt.Println(myErr.Code)
}

9. Pattern nâng cao với Interfaces

9.1. Dependency Injection

Interface rất hữu ích để triển khai dependency injection, giúp mã dễ kiểm thử.

Ví dụ:

type Logger interface {
    Log(message string)
}

type ConsoleLogger struct{}

func (c ConsoleLogger) Log(message string) {
    fmt.Println("Log:", message)
}

type Service struct {
    logger Logger
}

func (s Service) DoWork() {
    s.logger.Log("Working...")
}

func main() {
    s := Service{logger: ConsoleLogger{}}
    s.DoWork() // Output: Log: Working...
}
  • Trong test, bạn có thể thay ConsoleLogger bằng MockLogger mà không cần sửa Service.

9.2. Middleware Pattern

Interface thường được dùng trong middleware để xử lý chuỗi hành vi.

Ví dụ:

type Handler interface {
    ServeHTTP(http.ResponseWriter, *http.Request)
}

type Middleware func(Handler) Handler

func LoggingMiddleware(next Handler) Handler {
    return HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("Request received")
        next.ServeHTTP(w, r)
    })
}

9.3. Plugin System

Interface cho phép xây dựng hệ thống plugin linh hoạt.

Ví dụ:

type Plugin interface {
    Execute() string
}

type HelloPlugin struct{}

func (p HelloPlugin) Execute() string {
    return "Hello from plugin"
}

func main() {
    var p Plugin = HelloPlugin{}
    fmt.Println(p.Execute()) // Output: Hello from plugin
}

10. Interfaces và Generics

Từ Go 1.18, generics có thể kết hợp với interface để tăng tính linh hoạt.

Ví dụ:

type Container[T any] interface {
    Add(item T)
    Get() T
}

type IntContainer struct {
    items []int
}

func (c *IntContainer) Add(item int) {
    c.items = append(c.items, item)
}

func (c *IntContainer) Get() int {
    return c.items[0]
}

Lưu ý: Generics giảm sự phụ thuộc vào interface{}, nhưng interface vẫn hữu ích khi cần trừu tượng hóa hành vi.


11. Tối ưu hóa khi sử dụng Interfaces

  • Tránh lạm dụng interface{}: Dẫn đến mã khó bảo trì. Hãy dùng interface cụ thể hoặc generics.

  • Sử dụng interface nhỏ: Một interface chỉ nên chứa 1-2 phương thức để dễ triển khai và tái sử dụng.

  • Hiểu chi phí runtime: Interface có overhead nhỏ khi gọi phương thức (do dynamic dispatch). Với các đoạn mã hiệu năng cao, hãy cân nhắc sử dụng kiểu cụ thể.

  • Kiểm tra nil cẩn thận: Đặc biệt khi làm việc với interface chứa con trỏ.


12. Những lỗi thường gặp

  1. Quên kiểm tra nil: Gây panic khi gọi phương thức trên interface nil.

  2. Interface quá lớn: Làm mã phức tạp và khó bảo trì.

  3. Sử dụng interface{} không cần thiết: Làm mất type safety.

  4. Không tận dụng implicit implementation: Viết mã dư thừa để "khai báo" triển khai interface.


13. Ứng dụng thực tế

  • API Server: Interface để định nghĩa handler, middleware, hoặc repository.

  • Database Access: Interface như sql.DB để trừu tượng hóa kết nối cơ sở dữ liệu.

  • Testing: Mock implementation của interface để kiểm thử.

  • Plugin System: Interface để mở rộng chức năng mà không sửa mã nguồn.

Ví dụ thực tế với API:

type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

type UserService struct {
    repo UserRepository
}

func (s UserService) GetUser(id int) (*User, error) {
    return s.repo.FindByID(id)
}

15. Tài liệu tham khảo


Last updated