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ụ:
func main() {
num := 2
p := &num
fmt.Println(p)
}Con trỏ lưu trữ là địa chỉ của biến num
0xc00001c088Toá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ụ
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ụ:
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ụ:
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
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ụ:
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++:
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
0x31d99ff884Có 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ụ
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 new và make, 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.
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ỏ
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ụ:
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