Skip to content

Con trỏ trong Go

Go giữ lại con trỏ, ở một mức độ nào đó đảm bảo hiệu năng, đồng thời để GC tốt hơn và cân nhắc an toàn, lại hạn chế việc sử dụng con trỏ.

Tạo

Đối với con trỏ có hai toán tử thường dùng, một là toán tử lấy địa chỉ &, hai là toán tử giải tham chiếu *. Lấy địa chỉ của một biến, sẽ trả về con trỏ của kiểu tương ứng, ví dụ:

go
func main() {
   num := 2
   p := &num
   fmt.Println(p)
}

Con trỏ lưu trữ là địa chỉ của biến num

0xc00001c088

Toán tử giải tham chiếu thì có hai công dụng, thứ nhất là truy cập phần tử mà con trỏ trỏ đến, tức là giải tham chiếu, ví dụ

go
func main() {
  num := 2
  p := &num
  rawNum := *p
  fmt.Println(rawNum)
}

p là một con trỏ, giải tham chiếu kiểu con trỏ là có thể truy cập đến phần tử mà con trỏ trỏ đến. Còn một công dụng nữa là khai báo một con trỏ, ví dụ:

go
func main() {
   var numPtr *int
   fmt.Println(numPtr)
}
<nil>

*int tức đại diện cho kiểu của biến đó là một con trỏ kiểu int, nhưng con trỏ không thể chỉ khai báo, còn phải khởi tạo, cần phân bổ bộ nhớ cho nó, nếu không thì là một con trỏ rỗng, không thể sử dụng bình thường. Hoặc là sử dụng toán tử lấy địa chỉ để gán địa chỉ của biến khác cho con trỏ này, hoặc là sử dụng hàm tích hợp new để phân bổ thủ công, ví dụ:

go
func main() {
   var numPtr *int
   numPtr = new(int)
   fmt.Println(numPtr)
}

Thường dùng hơn là sử dụng biến ngắn

go
func main() {
   numPtr := new(int)
   fmt.Println(numPtr)
}

Hàm new chỉ có một tham số đó là kiểu, và trả về một con trỏ của kiểu tương ứng, hàm sẽ phân bổ bộ nhớ cho con trỏ đó, và con trỏ trỏ đến giá trị 0 của kiểu tương ứng, ví dụ:

go
func main() {
   fmt.Println(*new(string))
   fmt.Println(*new(int))
   fmt.Println(*new([5]int))
   fmt.Println(*new([]float64))
}

0
[0 0 0 0 0]
[]

Cấm phép toán con trỏ

Trong Go không hỗ trợ phép toán con trỏ, tức là con trỏ không thể dịch chuyển, trước hết xem một đoạn mã C++:

cpp
int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int *p = &arr[0];
    cout << &arr << endl
         << p << endl
         << p + 1 << endl
         << &arr[1] << endl;
}
0x31d99ff880
0x31d99ff880
0x31d99ff884
0x31d99ff884

Có thể thấy địa chỉ của mảng và địa chỉ của phần tử đầu tiên của mảng giống nhau, và sau khi thực hiện phép cộng một cho con trỏ, nó trỏ đến phần tử thứ hai của mảng. Trong Go mảng cũng như vậy, nhưng điểm khác biệt là con trỏ không thể dịch chuyển, ví dụ

go
func main() {
   arr := [5]int{0, 1, 2, 3, 4}
   p := &arr
   println(&arr[0])
   println(p)
   // Cố gắng thực hiện phép toán con trỏ
   p++
   fmt.Println(p)
}

Chương trình như vậy sẽ không thể biên dịch, báo lỗi như sau

main.go:10:2: invalid operation: p++ (non-numeric type *[5]int)

TIP

Thư viện chuẩn unsafe cung cấp nhiều thao tác dùng cho lập trình cấp thấp, trong đó bao gồm phép toán con trỏ, đến Thư viện chuẩn - unsafe để tìm hiểu chi tiết.

new và make

Trong vài phần trước đã rất nhiều lần nhắc đến hàm tích hợp newmake, hai cái này có chút tương tự, nhưng cũng có khác biệt, dưới đây ôn tập lại.

go
func new(Type) *Type
  • Giá trị trả về là con trỏ kiểu
  • Tham số nhận là kiểu
  • Chuyên dùng để phân bổ bộ nhớ cho con trỏ
go
func make(t Type, size ...IntegerType) Type
  • Giá trị trả về là giá trị, không phải con trỏ
  • Tham số đầu tiên nhận là kiểu, tham số bất định độ dài tùy thuộc vào kiểu truyền vào mà khác nhau
  • Chuyên dùng để phân bổ bộ nhớ cho slice, map, kênh.

Dưới đây là một số ví dụ:

go
new(int) // Con trỏ int
new(string) // Con trỏ string
new([]int) // Con trỏ slice int
make([]int, 10, 100) // Slice int độ dài 10, dung lượng 100
make(map[string]int, 10) // Map dung lượng 10
make(chan int, 10) // Kênh có đệm kích thước 10

Golang by www.golangdev.cn edit