+1

Con Trỏ (Pointers) Trong Go (Golang): Bản Chất, Ý Nghĩa Trong Backend Và Cạm Bẫy Thực Chiến

Trong Golang, con trỏ (pointer) được đại diện bởi ký hiệu * là một biến đặc biệt dùng để lưu trữ địa chỉ bộ nhớ của một biến khác, thay vì lưu trữ giá trị thực tế của biến đó. Vì Go mặc định truyền dữ liệu theo kiểu "Pass by Value" (tạo ra một bản sao khi truyền vào hàm), con trỏ là cơ chế duy nhất để các hàm có thể chia sẻ và thay đổi chung một vùng nhớ mà không cần nhân bản dữ liệu.


1. Bản Chất Của Ký Hiệu *&

Để làm chủ con trỏ trong Go, bạn chỉ cần phân định rạch ròi hai toán tử này:

  • & (Address-of): Lấy địa chỉ bộ nhớ của một biến đang có. Ví dụ: &user sẽ trả về địa chỉ ô nhớ của biến user.
  • * (Dereference / Pointer Type): Có hai tác dụng tùy vào ngữ cảnh:
    • Khi khai báo kiểu: Định nghĩa đây là một biến con trỏ (ví dụ: var p *int - p là con trỏ trỏ tới vùng nhớ chứa số nguyên).
    • Khi truy xuất: Trích xuất hoặc ghi đè giá trị nằm tại địa chỉ bộ nhớ mà con trỏ đang giữ (ví dụ: *p = 10 - gán giá trị 10 vào ô nhớ đó).

2. Tại Sao Lại Cần Con Trỏ Trong Hệ Thống Backend?

  • Thay đổi trạng thái dữ liệu gốc (Mutation): Khi bạn truyền một struct Order vào hàm processPayment(), nếu không dùng con trỏ, hàm sẽ chỉ thao tác trên bản copy. Mọi thay đổi trạng thái như order.Status = "PAID" sẽ biến mất khi thoát hàm. Dùng *Order đảm bảo dữ liệu gốc được cập nhật.
  • Tối ưu hiệu năng bộ nhớ (Performance): Truyền một struct chứa hàng trăm field (ví dụ: một payload JSON lớn) qua lại giữa các hàm sẽ liên tục copy dữ liệu, làm phình RAM và tốn CPU. Truyền con trỏ (chỉ nặng đúng 8 bytes trên hệ thống 64-bit) giúp tiết kiệm tài nguyên tuyệt đối.
  • Phân biệt giá trị Zero và Null (Nil Semantics): Trong Go, kiểu int mặc định là 0, kiểu string mặc định là "". Nếu bạn truy vấn Database và một trường bị bỏ trống, làm sao biết người dùng cố tình nhập số 0 hay họ không nhập gì? Dùng con trỏ *int. Nếu người dùng không nhập, biến sẽ mang giá trị nil. Nếu nhập 0, biến sẽ trỏ đến ô nhớ chứa số 0.

3. Minh Họa Bằng Mã Nguồn

package main

import "fmt"

// Hàm này nhận vào BẢN SAO (Pass by Value) -> Không thay đổi được dữ liệu gốc
func failUpdate(age int) {
    age = 30
}

// Hàm này nhận vào ĐỊA CHỈ BỘ NHỚ (Pass by Reference)
func successUpdate(age *int) {
    *age = 30 // Dùng * để can thiệp trực tiếp vào giá trị tại ô nhớ
}

func main() {
    myAge := 22
    
    failUpdate(myAge)
    fmt.Println("Sau khi failUpdate:", myAge) // Vẫn in ra 22
    
    successUpdate(&myAge) // Truyền địa chỉ của biến myAge
    fmt.Println("Sau khi successUpdate:", myAge) // In ra 30
}

4. Cạm Bẫy Cần Tránh Khi Dùng Con Trỏ

  • Tránh dùng cho Slice và Map: Slice và Map trong Go bản chất bên dưới đã là các tham chiếu (reference types) chứa con trỏ. Việc truyền *[]int hay *map[string]string là thừa thãi và gây rối rắm mã nguồn.
  • Gánh nặng bộ dọn rác (Garbage Collection Overhead): Đừng lạm dụng con trỏ cho các biến quá nhỏ bé (như số nguyên int thông thường chạy vòng lặp). Biến thông thường được cấp phát trên bộ nhớ Stack siêu nhanh. Khi dùng con trỏ không hợp lý, Go có thể ép cấp phát bộ nhớ trên Heap (Escape Analysis), làm chậm chương trình do Garbage Collector phải dọn dẹp liên tục.

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í