Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách con trỏ (pointer) hoạt động trong Go và chúng ta cũng sẽ hiểu con trỏ trong Go khác với con trỏ trong các ngôn ngữ khác như C và C++ như thế nào.
Con trỏ (pointer) là một biến lưu trữ địa chỉ bộ nhớ của một biến khác.
Trong hình minh họa trên, biến b
có giá trị 156
và được lưu trữ tại địa chỉ bộ nhớ 0x1040a124
. Biến a
lưu giữ địa chỉ của biến b
. Bây giờ biến a
được cho là trỏ đến biến b
.
*T là kiểu của biến con trỏ mà nó trỏ đến một giá trị của kiểu T.
Hãy viết một chương trình khai báo một con trỏ.
package main
import (
"fmt"
)
func main() {
b := 255
var a *int = &b
fmt.Printf("Type of a is %T\n", a)
fmt.Println("address of b is", a)
}
Chạy chương trình trong playground
Toán tử &
được sử dụng để có được địa chỉ của một biến. Trong dòng số 9 của chương trình trên chúng ta đang gán địa chỉ của biến b
cho biến a
có kiểu *int
. Bây giờ biến a
được cho là trỏ đến biến b
. Khi chúng ta in giá trị của biến a
, địa chỉ của biến b
sẽ được in ra. Chương trình này xuất ra:
Type of a is *int
address of b is 0x1040a124
Lưu ý: bạn có thể nhận được một địa chỉ khác cho biến b vì vị trí của biến b có thể ở bất kỳ đâu trong bộ nhớ.
Zero value của một con trỏ là nil
.
package main
import (
"fmt"
)
func main() {
a := 25
var b *int
if b == nil {
fmt.Println("b is", b)
b = &a
fmt.Println("b after initialization is", b)
}
}
Chạy chương trình trong playground
Biến b
ban đầu là nil trong chương trình trên và sau đó nó được gán cho địa chỉ của biến a
. Chương trình này xuất ra:
b is <nil>
b after initialisation is 0x1040a124
Go cũng cung cấp một hàm tiện ích lànew
để tạo con trỏ. Hàm new
cần một kiểu dữ liệu như là một đối số và trả về con trỏ trỏ đến địa chỉ vùng nhớ vừa được cấp phát cho zero value của kiểu dữ liệu được truyền vào làm đối số.
Ví dụ sau đây sẽ làm cho mọi thứ rõ ràng hơn.
package main
import (
"fmt"
)
func main() {
size := new(int)
fmt.Printf("Size value is %d, type is %T, address is %v\n", *size, size, size)
*size = 85
fmt.Println("New size value is", *size)
}
Chạy chương trình trong playground
Trong chương trình trên, ở dòng số 8 chúng ta sử dụng hàm new
để tạo một con trỏ kiểu int
. Hàm này sẽ trả về một con trỏ đến một zero value mới được cấp phát của kiểu int
. Zero value của kiểu int
là 0
. Do đó biến size
sẽ là kiểu *int
và sẽ trỏ đến giá trị 0
tức là *size
sẽ là 0
.
Chương trình trên sẽ in ra kết quả sau:
Size value is 0, type is *int, address is 0x414020
New size value is 85
Tham chiếu đến một con trỏ có nghĩa là truy cập giá trị của biến mà con trỏ trỏ đến. *a
là cú pháp để tham chiếu đến giá trị của con trỏ a
.
Hãy xem điều này hoạt động như thế nào trong một chương trình.
package main
import (
"fmt"
)
func main() {
b := 255
a := &b
fmt.Println("address of b is", a)
fmt.Println("value of b is", *a)
}
Chạy chương trình trong playground
Ở dòng số 10 của chương trình trên, chúng ta định nghĩa con trỏ a
và in giá trị của nó. Như mong đợi, nó in ra giá trị của biến b
. Đầu ra của chương trình là:
address of b is 0x1040a124
value of b is 255
Hãy viết thêm một chương trình mà chúng ta thay đổi giá trị biến b
bằng cách sử dụng con trỏ.
package main
import (
"fmt"
)
func main() {
b := 255
a := &b
fmt.Println("address of b is", a)
fmt.Println("value of b is", *a)
*a++
fmt.Println("new value of b is", b)
}
Chạy chương trình trong playground
Ở dòng số 12 của chương trình trên, chúng ta tăng giá trị được trỏ a
thêm 1, điều này sẽ thay đổi giá trị của biến b vì con trỏ a trỏ tới biến b. Do đó giá trị của biến b trở thành 256. Đầu ra của chương trình là:
address of b is 0x1040a124
value of b is 255
new value of b is 256
package main
import (
"fmt"
)
func change(val *int) {
*val = 55
}
func main() {
a := 58
fmt.Println("value of a before function call is",a)
b := &a
change(b)
fmt.Println("value of a after function call is", a)
}
Chạy chương trình trong playground
Trong chương trình trên, ở dòng số 14 chúng ta đang truyền biến con trỏ b
giữ địa chỉ của biến a
cho hàm change
. Bên trong hàm change
, giá trị của biến a được thay đổi bằng cách sử dụng tham chiếu ở dòng số 8. Chương trình này xuất ra kết quả sau:
value of a before function call is 58
value of a after function call is 55
Hoàn toàn hợp pháp khi một hàm trả về một con trỏ của một biến cục bộ. Trình biên dịch Go đủ thông minh và nó sẽ cấp phát biến này trên heap.
package main
import (
"fmt"
)
func hello() *int {
i := 5
return &i
}
func main() {
d := hello()
fmt.Println("Value of d", *d)
}
Chạy chương trình trong playground
Ở dòng số 9 của chương trình trên, chúng ta trả về địa chỉ của biến cục bộ i
từ hàm hello
.
Hành vi của mã này không được xác định trong các ngôn ngữ lập trình như C và C++ vì biến i
vượt ra ngoài phạm vi khi hàm hello
trả về. Nhưng trong trường hợp Go, trình biên dịch thực hiện phân tích thoát và phân bổ i
trên heap khi địa chỉ thoát khỏi phạm vi cục bộ.
Do đó chương trình này sẽ hoạt động và nó sẽ in ra kết quả sau:
Value of d 5
Giả sử rằng chúng ta muốn thực hiện một số sửa đổi đối với một mảng bên trong hàm và những thay đổi được thực hiện đối với mảng đó bên trong hàm sẽ được áp dụng cho mảng ở ngoài hàm. Một cách để làm điều này là truyền một con trỏ đến một mảng làm đối số cho hàm.
package main
import (
"fmt"
)
func modify(arr *[3]int) {
(*arr)[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
Chạy chương trình trong sân chơi
Ở dòng số 13 của chương trình trên, chúng ta đang truyền địa chỉ của mảng a
cho hàm modify
. Ở dòng số 8 trong hàm modify
chúng ta đang tham chiếu đến mảng và gán 90
cho phần tử đầu tiên của mảng. Chương trình này xuất ra:
[90 90 91]
a[x] là viết tắt của (*a) [x]. Vì vậy (*arr) [0] trong chương trình trên có thể được thay thế bằng arr[0].
Hãy viết lại chương trình trên bằng cú pháp viết tắt này.
package main
import (
"fmt"
)
func modify(arr *[3]int) {
arr[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
Chạy chương trình trong playground
Chương trình này cũng xuất ra kết quả tương tự:
[90 90 91]
Mặc dù cách này để truyền một con trỏ đến một mảng làm đối số cho một hàm và thực hiện sửa đổi nó hoạt động, nhưng đó không phải là cách hay để đạt được điều này trong Go. Chúng tôi có thể sử dụng slice để đạt được điều này.
Hãy viết lại cùng một chương trình bằng cách sử dụng slice.
package main
import (
"fmt"
)
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(a[:])
fmt.Println(a)
}
Chạy chương trình trong playground
Ở dòng số 13 của chương trình trên, chúng ta truyền một slice cho hàm modify
. Phần tử đầu tiên của slice được thay đổi thành 90
bên trong hàm modify
.
Chương trình này cũng xuất ra cùng một kết quả:
[90 90 91]
Vì vậy, hãy quên việc truyền con trỏ trỏ đến mảng vào một hàm và thay vào đó sử dụng slice :).
Mã này rõ ràng hơn nhiều và là cách làm hay trong Go :).
Go không hỗ trợ con trỏ số học hiện có trong các ngôn ngữ khác như C và C++.
package main
func main() {
b := [...]int{109, 110, 111}
p := &b
p++
}
Chạy chương trình trong playground
Chương trình trên sẽ ném ra lỗi biên dịch:
main.go:6: invalid operation: p++ (non-numeric type *[3]int)
Trong hướng dẫn này, bạn đã tìm hiểu cách con trỏ hoạt động trong Go.
Như thường lệ, cảm ơn bạn đã đọc. Nếu thấy bài viết này hay và hưu ích, bạn hãy chia sẻ nó tới những người bạn của mình.
Bạn có thể vui lòng tắt trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi duy trì hoạt động của trang web.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu phương thức (method) trong Go là gì? Cú pháp khai báo phương thức, so sánh phương thức với hàm, ... trong Go.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu struct là gì, cách khai báo và sử dụng một struct trong Go, struct ẩn danh, so sanh hai struct, ...
Chuỗi (string) xứng đáng được đề cập đặc biệt trong Go vì chúng khác biệt trong cách triển khai khi so sánh với các ngôn ngữ khác.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu map là gì, cú pháp khai báo map, thêm phần tử, xóa phần tử, duyệt các phần tử, ... của map trong Go.