0

Series Go Backend: Methods - Biến Struct thành Object với Pointer Receiver và Value Receiver

Ở bài trước, chúng ta đã biết cách dùng Struct để gom nhóm dữ liệu. Nhưng trong lập trình hướng đối tượng (OOP), một đối tượng không chỉ có thuộc tính (dữ liệu) mà còn phải có các hành vi (phương thức). Vì Go không có class, làm thế nào để chúng ta "đính kèm" một hàm vào một Struct?

Câu trả lời chính là Methods (Phương thức) kết hợp với Receiver (Bên nhận).

1. Method và Cú pháp Receiver

Method trong Go bản chất vẫn là một hàm (function) thông thường, nhưng nó có thêm một tham số đặc biệt nằm giữa từ khóa func và tên hàm. Tham số này được gọi là Receiver.

Nhờ có Receiver, hàm đó sẽ được "gắn" vào kiểu dữ liệu tương ứng, và bạn có thể gọi nó bằng cú pháp dấu chấm . giống hệt như gọi phương thức của object trong C++ hay PHP.

Go

type User struct {
    Username string
    Status   string
}

// Đây là một METHOD. Tham số (u User) chính là Receiver
func (u User) GetProfile() string {
    return "User: " + u.Username + " - Status: " + u.Status
}

func main() {
    user := User{Username: "hieudev", Status: "Active"}
    
    // Gọi method bằng cú pháp dấu chấm
    fmt.Println(user.GetProfile()) 
}

2. Value Receiver: Hoạt động trên bản sao

Khi khai báo method như ví dụ trên (u User), Go sử dụng Value Receiver.

Đúng như nguyên tắc "Pass-by-value" mà chúng ta đã học ở Bài 9: khi bạn gọi method này, Go sẽ tạo ra một bản sao (copy) của Struct User và truyền vào method.

Go

// Cố gắng thay đổi trạng thái user bằng Value Receiver
func (u User) Deactivate() {
    u.Status = "Banned" // Chỉ đang thay đổi trên bản sao!
}

func main() {
    user := User{Username: "hieudev", Status: "Active"}
    user.Deactivate()
    
    // Trạng thái gốc không hề thay đổi
    fmt.Println(user.Status) // Vẫn in ra: Active
}

Khi nào dùng Value Receiver?

  • Khi method chỉ dùng để đọc dữ liệu (Read-only), tính toán, hoặc in ấn mà không cần chỉnh sửa giá trị gốc của Struct.

  • Khi Struct cực kỳ nhỏ (như một custom type của int hoặc float). Với Struct lớn, việc copy bộ nhớ liên tục sẽ làm giảm hiệu năng.

3. Pointer Receiver: Kẻ nắm giữ quyền lực thực sự

Để một method có thể thay đổi (mutate) dữ liệu gốc của Struct, chúng ta phải sử dụng Pointer Receiver bằng cách thêm dấu * vào trước kiểu dữ liệu.

Go

// Khai báo Pointer Receiver: (u *User)
func (u *User) Deactivate() {
    u.Status = "Banned" // Thay đổi trực tiếp trên vùng nhớ gốc!
}

func main() {
    user := User{Username: "hieudev", Status: "Active"}
    
    // Go tự động ngầm hiểu là (&user).Deactivate()
    user.Deactivate() 
    
    // Trạng thái gốc đã bị thay đổi thành công
    fmt.Println(user.Status) // In ra: Banned
}

Sự "nuông chiều" của trình biên dịch Go: Nếu bạn để ý, user đang là một biến giá trị (Value), nhưng gọi method của Pointer Receiver Deactivate() lại không hề bị lỗi. Đáng lẽ ra ta phải viết là (&user).Deactivate().

Thực tế, trình biên dịch Go đã tự động thêm dấu & hoặc * ngầm định ở dưới nền để giúp code của bạn gọn gàng hơn. Miễn là biến đó có thể lấy được địa chỉ (addressable), Go sẽ tự động convert qua lại giữa Value và Pointer khi gọi method.

4. Nguyên tắc "Vàng" khi chọn Receiver trong Backend

Trong thực tế xây dựng các hệ thống Backend (như Microservices bằng Go), các thực thể Database (Model), các Service, Repository hay Controller thường là những Struct khá lớn, chứa nhiều trường hoặc cấu hình kết nối (Database connection, Redis client).

Do đó, cộng đồng Go tuân theo những luật bất thành văn sau:

  1. Mặc định dùng Pointer Receiver: Trong 90% trường hợp, hãy dùng Pointer Receiver cho các Struct lớn. Nó vừa giúp thay đổi được trạng thái, vừa tránh chi phí copy rác vào RAM.

  2. Tính nhất quán (Consistency): Nếu Struct của bạn có 5 methods, và 1 method BẮT BUỘC phải dùng Pointer Receiver (vì cần sửa dữ liệu), thì 4 method còn lại cũng PHẢI dùng Pointer Receiver, cho dù chúng chỉ đọc dữ liệu. Đừng bao giờ mix lộn xộn Value và Pointer Receiver trên cùng một Struct.

  3. Trường hợp hiếm hoi dùng Value Receiver: Các object nhỏ mang tính bất biến (Immutable) như struct đại diện cho Tọa độ (Point x, y), hoặc struct đại diện cho Khoảng thời gian (Time.Duration). Khi chúng thay đổi, ta muốn tạo ra một object mới hoàn toàn chứ không đè lên cái cũ.

Việc thiết kế hệ thống Methods chuẩn mực sẽ là bước đệm hoàn hảo để chúng ta tiến vào một trong những khái niệm làm nên tên tuổi của Go ở bài tiếp theo: Interfaces.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí