Facebook Pixel

Golang Interface - Những lỗi sai thường gặp và giải pháp

13 Jun, 2023

Liệu bạn đã dùng Golang Interface đúng cách và hiệu quả? Bài viết này mình sẽ hướng dẫn chi tiết để các bạn dễ dàng sử dụng trong các service Golang nhé

Golang Interface - Những lỗi sai thường gặp và giải pháp

Mục Lục

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.

Go
type Speaker interface {
    Speak() string
}
Định nghĩa Stringer Interface trong Golang

Đ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:

Go
package main

import (
	"fmt"
)

type Speaker interface {
    Speak() string
}

type Foo struct{}

func (Foo) Speak() string {
	return "Hello, I am Foo"
}

func main() {
	var someSpeaker Speaker = Foo{}
	fmt.Println(someSpeaker.Speak())
}
Run thử tại đây: https://play.golang.com/p/Fm7QaE6KgHd

Đặ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{}:

Go
package main

import "fmt"

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}
Nguồn: https://go.dev/tour/methods/14

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.

Go
package main

import "fmt"

func main() {
    product := make(map[string]interface{}, 0)

    product["name"] = "Iphone 13 Pro Max"
    product["price"] = 31000000
    product["quantity"] = 40

    fmt.Println(product)
}
Ứng dụng Interface trong map[string]interface{}

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:

Go
package main

import "fmt"

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // panic
	fmt.Println(f)
}

Nguồn: https://go.dev/tour/methods/15

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:

Go
package main

import (
	"fmt"
)

type Speaker interface {
	Speak() string
}

type Foo struct{}

func (Foo) Speak() string {
	return "Hello, I am Foo"
}

type Bar struct{}

func (*Bar) Speak() string {
	return "Hello, I am Bar"
}

func main() {
	var someSpeaker Speaker = Foo{}

	fmt.Println(someSpeaker.Speak())
	
	someSpeaker = &Foo{} // will be OK
	
	fmt.Println(someSpeaker.Speak())
	
	someSpeaker = Bar{} // PANIC HERE
	
	fmt.Println(someSpeaker.Speak())
}
Run thử tại: https://play.golang.com/p/YX5Zghh2Ab4

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:

Go
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:

Go
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.

Go
package b

type Speaker interface {
    Speak() string
}

func someFunc(spk Speaker) {
	spk.Speak()
}
Khai báo Interface tại nơi sử dụng

Speakerpackage 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.

Go
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é:

Go
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:

Go
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ủ đề:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab