0

Go Zero to Hero - Bài 28: Khái niệm Con trỏ (Pointers) trong Go

Trong lập trình hệ thống, Con trỏ (Pointer) là một trong những khái niệm nền tảng quan trọng nhất. Golang mang đến một cơ chế quản lý con trỏ cực kỳ mạnh mẽ, giữ được hiệu năng cao của C/C++ nhưng lại an toàn tuyệt đối.


1. Con trỏ là gì? (Ví dụ Bản đồ và Ngôi nhà)

Hãy tưởng tượng bạn có một ngôi nhà (là một Biến), bên trong ngôi nhà đó chứa một chiếc tivi (là Giá trị của biến).

Để mời ai đó đến xem tivi, bạn có hai cách:

  • Copy-paste (Truyền Tham Trị): Bạn xây một ngôi nhà y hệt, mua một cái tivi y hệt, rồi bê cả cái nhà đó đưa cho người ta. Quá tốn kém (Tốn RAM!).
  • Đưa địa chỉ (Truyền Con Trỏ): Bạn chỉ cần viết địa chỉ nhà (ví dụ: 123 Lê Lợi) ra một mảnh giấy và đưa cho người ta. Họ sẽ cầm mảnh giấy đó, tìm đến đúng nhà bạn và xem chung cái tivi đó.

Con trỏ (Pointer) chính là mảnh giấy ghi địa chỉ đó. Nó là một biến đặc biệt, không lưu trữ giá trị thực (như số 10 hay chuỗi "Hello"), mà nó lưu trữ Địa chỉ bộ nhớ của một biến khác.


2. Cặp bài trùng & và *

Để làm việc với Con trỏ trong Go, bạn chỉ cần nhớ đúng 2 ký tự:

  • & (Ampersand - Dấu và): Dùng để LẤY ĐỊA CHỈ. (Đọc là: Địa chỉ của...).
  • * (Asterisk - Dấu sao): Dùng để Khai báo kiểu con trỏ, hoặc để TRUY CẬP VÀO GIÁ TRỊ tại địa chỉ đó (Giải tham chiếu - Dereference).

Hãy xem code thực chiến để thấy sự kỳ diệu:

package main

import "fmt"

func main() {
    // 1. Biến thông thường
    age := 21 
    
    // 2. Lấy địa chỉ của biến age bằng dấu &
    // pAge là một con trỏ có kiểu là *int (Con trỏ trỏ tới một số nguyên)
    pAge := &age 
    
    fmt.Println("Giá trị của age:", age)       // In ra: 21
    fmt.Println("Địa chỉ của age:", pAge)      // In ra: 0xc00001a0b8 (Một vùng nhớ Hexadecimal)
    
    // 3. Giải tham chiếu (Dereference) bằng dấu *
    // Thêm * trước pAge nghĩa là: "Hãy đi đến địa chỉ đó và lôi giá trị ra đây"
    fmt.Println("Giá trị tại địa chỉ pAge:", *pAge) // In ra: 21
    
    // 4. Thay đổi giá trị thông qua con trỏ
    *pAge = 99 
    
    // BÙM! Biến age gốc đã bị thay đổi theo
    fmt.Println("Giá trị mới của age:", age) // In ra: 99
}

3. Ứng dụng sống còn: Truyền Con trỏ vào Hàm

Mặc định trong Go, mọi thứ truyền vào hàm đều là Pass-by-value (Copy giá trị).

Nếu bạn muốn một hàm thay đổi trạng thái của biến gốc, hoặc bạn muốn truyền một khối dữ liệu khổng lồ (như một Struct chứa toàn bộ thông tin User) vào hàm mà không làm phình RAM, bạn BẮT BUỘC phải truyền Con trỏ.

package main

import "fmt"

// Hàm này nhận vào COPY của điểm số (Không thay đổi được biến gốc)
func cheatScoreFailed(score int) {
    score = 100 // Chỉ đổi bản copy cục bộ
}

// Hàm này nhận vào ĐỊA CHỈ của điểm số (Thay đổi được biến gốc)
func cheatScoreSuccess(pScore *int) {
    *pScore = 100 // Lần theo địa chỉ và sửa lại giá trị thực tế
}

func main() {
    myScore := 50
    
    cheatScoreFailed(myScore)
    fmt.Println("Sau khi gian lận thất bại:", myScore) // Vẫn là 50
    
    // Truyền địa chỉ của myScore vào hàm
    cheatScoreSuccess(&myScore) 
    fmt.Println("Sau khi gian lận thành công:", myScore) // Lên đỉnh: 100
}

4. Tại sao Con trỏ của Go lại được gọi là "Hiền lành"?

Nếu bạn từng học C/C++, con trỏ là nguyên nhân gây ra 90% các lỗ hổng bảo mật và các cú sập ứng dụng (Segmentation Fault) không rõ nguyên nhân. Đó là vì C/C++ cho phép Pointer Arithmetic (Toán học Con trỏ).

Trong C++, bạn có thể làm trò này: pAge++ (cộng con trỏ thêm 1) để ép hệ thống nhảy sang địa chỉ ô nhớ kế tiếp. Nếu ô nhớ kế tiếp chứa dữ liệu mật khẩu của hệ thống, bạn vừa hack thành công bộ nhớ!

Go tuyệt đối nghiêm cấm điều này.

Trong Go, bạn không thể thực hiện các phép cộng, trừ, nhân, chia lên một con trỏ. Con trỏ trong Go chỉ đóng vai trò thuần túy là "người vận chuyển" (tham chiếu). Điều này giúp Go giữ được hiệu năng cao nhưng lại an toàn với bộ nhớ không kém gì Java.


5. Bẫy tử thần: panic: runtime error: invalid memory address or nil pointer dereference

Bạn sẽ gặp dòng chữ đỏ chết chóc này hàng chục lần trong sự nghiệp code Go.

Giống như mọi kiểu dữ liệu, con trỏ cũng có Zero Value. Zero Value của con trỏ là nil (vô giá trị, không trỏ đi đâu cả).

Nếu bạn cố tình giải tham chiếu (*) một con trỏ đang mang giá trị nil, server của bạn sẽ Crash (sập hoàn toàn) ngay lập tức!

func main() {
    var pName *string // Con trỏ pName đang là nil
    
    // Nếu bạn kiểm tra trước thì không sao
    if pName != nil {
        fmt.Println(*pName)
    }
    
    // 🚨 NHƯNG NẾU CỐ TÌNH GỌI:
    // *pName = "Hoàng" // SERVER SẬP NGAY LẬP TỨC!
}

💡 Luôn nhớ nguyên tắc: Bất cứ khi nào nhận được một con trỏ, hãy kiểm tra if ptr != nil trước khi tương tác với nó.


Tổng kết

Tóm lại những kiến thức sinh tử về Con trỏ:

  1. Bản chất: Là một biến lưu địa chỉ ô nhớ của biến khác.
  2. Cú pháp: & để lấy địa chỉ, * để khai báo kiểu con trỏ và thao tác với giá trị tại địa chỉ đó.
  3. Tối ưu: Dùng để tránh việc copy những khối dữ liệu lớn khi truyền qua lại giữa các hàm, và cho phép hàm thay đổi dữ liệu gốc.
  4. An toàn: Go không có Pointer Arithmetic. Nhưng hãy cẩn thận tuyệt đối với nil pointer dereference.

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í