Hướng dẫn cơ bản về con trỏ trong Go: Làm chủ bộ nhớ

Xin chào các bạn future Go developers! Hôm nay, chúng ta sẽ bắt đầu một hành trình đầy thú vị vào thế giới của các con trỏ trong Go. Đừng lo lắng nếu bạn chưa bao giờ lập trình trước đây - tôi sẽ là người hướng dẫn thân thiện của bạn, và chúng ta sẽ cùng nhau từng bước. Cuối cùng của bài hướng dẫn này, bạn sẽ có thể sử dụng con trỏ trong Go như một chuyên gia!

Go - Pointers

什么是指针?

Hãy tưởng tượng bạn có một bản đồ kho báu. Thay vì viết toàn bộ mô tả về vị trí của kho báu, bạn chỉ cần viết lại các tọa độ. Đó chính là bản chất của con trỏ trong lập trình - nó là một biến lưu trữ địa chỉ bộ nhớ của một biến khác.

Trong Go, các con trỏ vô cùng hữu ích. Chúng cho phép chúng ta trực tiếp manipulatie dữ liệu được lưu trữ trong một vị trí bộ nhớ cụ thể, điều này có thể dẫn đến các chương trình hiệu quả và mạnh mẽ hơn.

Hãy bắt đầu với một ví dụ đơn giản:

package main

import "fmt"

func main() {
x := 42
p := &x
fmt.Println("Giá trị của x:", x)
fmt.Println("Địa chỉ của x:", p)
}

Khi bạn chạy đoạn mã này, bạn sẽ thấy điều gì đó như sau:

Giá trị của x: 42
Địa chỉ của x: 0xc0000b4008

Trong ví dụ này, x là biến thông thường chứa giá trị 42. Оператор & được sử dụng để lấy địa chỉ bộ nhớ của x, sau đó chúng ta lưu trữ nó trong p. Biến p bây giờ là con trỏ đến x.

Làm thế nào để sử dụng các con trỏ?

Bây giờ chúng ta đã biết con trỏ là gì, hãy cùng xem cách chúng ta có thể sử dụng chúng. Điều kỳ diệu thực sự xảy ra khi chúng ta muốn truy cập hoặc thay đổi giá trị mà con trỏ đang chỉ đến. Chúng ta làm điều này bằng cách sử dụng оператор *.

package main

import "fmt"

func main() {
x := 42
p := &x

fmt.Println("Giá trị của x:", x)
fmt.Println("Giá trị mà p chỉ đến:", *p)

*p = 24
fmt.Println("Giá trị mới của x:", x)
}

Kết quả:

Giá trị của x: 42
Giá trị mà p chỉ đến: 42
Giá trị mới của x: 24

Trong ví dụ này, chúng ta sử dụng *p để truy cập giá trị mà p đang chỉ đến (đó là x). Chúng ta có thể đọc giá trị này, và chúng ta cũng có thể thay đổi nó. Khi chúng ta đặt *p = 24, chúng ta thực sự đang thay đổi giá trị của x!

Phương thức con trỏ

Đây là một sự thật thú vị: trong Go, bạn có thể xác định phương thức với các bộ nhận con trỏ. Điều này cho phép phương thức thay đổi giá trị mà bộ nhận chỉ đến. Hãy cùng xem một ví dụ:

package main

import "fmt"

type Person struct {
Name string
Age  int
}

func (p *Person) HaveBirthday() {
p.Age++
}

func main() {
alice := Person{Name: "Alice", Age: 30}
fmt.Printf("%s là %d tuổi\n", alice.Name, alice.Age)

alice.HaveBirthday()
fmt.Printf("Sau sinh nhật, %s là %d tuổi\n", alice.Name, alice.Age)
}

Kết quả:

Alice là 30 tuổi
Sau sinh nhật, Alice là 31 tuổi

Trong ví dụ này, phương thức HaveBirthday có một bộ nhận con trỏ (p *Person). Điều này có nghĩa là nó có thể thay đổi cấu trúc Person mà nó được gọi trên. Khi chúng ta gọi alice.HaveBirthday(), nó tăng tuổi của Alice lên 1.

Con trỏ nil trong Go

Giống như người bạn luôn quên mang snack đến buổi xem phim, các con trỏ đôi khi có thể chỉ đến không có gì. Trong Go, chúng ta gọi điều này là con trỏ nil.

package main

import "fmt"

func main() {
var p *int
fmt.Println("Giá trị của p:", p)

if p == nil {
fmt.Println("p là con trỏ nil")
}
}

Kết quả:

Giá trị của p: <nil>
p là con trỏ nil

Hãy cẩn thận với các con trỏ nil! Nếu bạn cố gắng suy luận một con trỏ nil (tức là sử dụng *p khi p là nil), chương trình của bạn sẽ crash nhanh hơn bạn có thể nói "segfault".

Con trỏ trong Go chi tiết

Bây giờ chúng ta đã bao gồm các nguyên tắc cơ bản, hãy cùng đi sâu hơn vào một số khái niệm con trỏ phức tạp hơn.

Con trỏ đến con trỏ

Đúng vậy, bạn đã đọc đúng! Chúng ta có thể có các con trỏ chỉ đến các con trỏ khác. Đó là như một cuộc phiêu lưu trong cuộc phiêu lưu, nhưng với các địa chỉ bộ nhớ.

package main

import "fmt"

func main() {
x := 42
p := &x
pp := &p

fmt.Println("Giá trị của x:", x)
fmt.Println("Giá trị mà p chỉ đến:", *p)
fmt.Println("Giá trị mà pp chỉ đến:", **pp)
}

Kết quả:

Giá trị của x: 42
Giá trị mà p chỉ đến: 42
Giá trị mà pp chỉ đến: 42

Trong ví dụ này, pp là một con trỏ đến một con trỏ. Chúng ta sử dụng **pp để truy cập giá trị mà nó cuối cùng chỉ đến.

Con trỏ và slices

Slices trong Go đã là một loại con trỏ, điều này có thể dẫn đến một số hành vi thú vị:

package main

import "fmt"

func main() {
slice1 := []int{1, 2, 3}
slice2 := slice1

slice2[0] = 999

fmt.Println("slice1:", slice1)
fmt.Println("slice2:", slice2)
}

Kết quả:

slice1: [999 2 3]
slice2: [999 2 3]

Ngay cả khi chúng ta chỉ thay đổi slice2, slice1 cũng thay đổi. Điều này là vì slices là các loại tham chiếu trong Go, có nghĩa là chúng hành xử một chút như các con trỏ đằng sau hậu trường.

Các phương thức con trỏ phổ biến

Dưới đây là bảng các thao tác con trỏ phổ biến trong Go:

Thao tác Mô tả Ví dụ
& Lấy địa chỉ của một biến p := &x
* Suy luận một con trỏ y := *p
new() Phân bổ bộ nhớ và trả về một con trỏ p := new(int)
make() Tạo slices, maps và channels s := make([]int, 5)

Nhớ rằng, thực hành làm cho hoàn hảo! Đừng ngần ngại thử nghiệm với các khái niệm này trong mã của riêng bạn.

Cuối cùng, các con trỏ trong Go là những công cụ mạnh mẽ cho phép chúng ta manipulatie bộ nhớ trực tiếp. Chúng có thể làm cho các chương trình của chúng ta hiệu quả hơn và cho phép chúng ta tạo ra các cấu trúc dữ liệu phức tạp. Chỉ cần nhớ rằng hãy xử lý chúng một cách cẩn thận - với quyền lực lớn cũng có trách nhiệm lớn!

Chúc các bạn lập trình vui vẻ, và mong rằng các con trỏ của bạn luôn chỉ đúng hướng!

Credits: Image by storyset