, September 29, 2022

0 kết quả được tìm thấy

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


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

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

Đị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ừ https://go.dev/tour/methods/9

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.

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:

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

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.

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:

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

Dựa trên kinh nghiệm cá nhân của mình sẽ có 2 vấn đề mà các Gopher thường gặp:

1. 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:

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.

2. Interface nên được khai báo ở tại nơi sử dụng nó

Vấn đề này mình 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.

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.

3. 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
}

Hy vọng qua bài này mình đã giúp các bạn hiểu được Interface Golang và cách dùng tốt hơn nhé. Mình sẽ có những demo Interface thực tế trong các service Golang: cho kiến trúc và Unit Test.

Hẹn gặp lại các bạn trong các bài tiếp theo.

Clean Architecture - Ưu nhược và cách dùng hợp lý
Clean Architecture là một kiến trúc ứng dụng rất nổi tiếng dựa trên nguyên lý loại bỏ sự lệ thuộc giữa các đối tượng cũng như các layer trong ứng dụng. Nguyên lý này kế thừa và phát triển trên Dependency Inversion
Ứng dụng Clean Architecture cho service Golang REST API
Trong bài viết này mình sẽ cung cấp một ví dụ cụ thể cho việc vận dụng nguyên tắc và tư duy Clean Architecture cho một service REST API Golang
Lập trình REST API TODO List với Golang - Từ UI tới triển khai
Hướng dẫn từng bước lập trình REST API với ngôn ngữ Go (Golang) sử dụng GIN và GORM

Bài viết liên quan

Buffered Channel là gì? Thường xuyên bị hỏi trong phỏng vấn Golang Dev

Buffered Channel là một channel trong Golang có khả năng lưu trữ được dữ liệu bên trong nó. Khả năng này được gọi là sức chứa (capacity)....

Buffered Channel là gì? Thường xuyên bị hỏi trong phỏng vấn Golang Dev
Golang Channel là gì? Các ứng dụng Channel trong Golang

Channel là kênh giao tiếp trung gian giữa các Goroutines có thể gởi và nhận dữ liệu cho nhau một cách an toàn thông qua cơ chế lock-free....

Golang Channel là gì? Các ứng dụng Channel trong Golang
Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang

Goroutines bản chất là các hàm được thực thi một các độc lập và đồng thời trong Golang cực kỳ đơn giản và gọn nhẹ so với các Thread truyền thống....

Goroutines là gì? Lập trình concurrency chưa bao giờ dễ như Golang
Rust vs Go - Ngôn ngữ nào tốt nhất năm 2022

Rust và Go hiện là 2 ứng cử viên nặng ký và tin dùng cho các hệ thống lớn. Rust và Golang trông có vẻ tương tự nhau nhưng chúng cũng rất khác biệt...

Rust vs Go - Ngôn ngữ nào tốt nhất năm 2022
Ứng dụng Clean Architecture cho service Golang REST API

Trong bài viết này mình sẽ cung cấp một ví dụ cụ thể cho việc vận dụng nguyên tắc và tư duy Clean Architecture cho một service REST API Golang...

Ứng dụng Clean Architecture cho service Golang REST API
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.