Con trỏ trong Go

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ỏ là gì?

Con trỏ (pointer) là một biến lưu trữ địa chỉ bộ nhớ của một biến khác.

Con trỏ là gì?

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.

Khai báo con trỏ trong Go

*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ỏ

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  

Tạo con trỏ bằng hàm new

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 int0. 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ỏ

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  

Truyền con trỏ vào một hàm

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  

Hàm trả về con trỏ

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  

Truyền một con trỏ trỏ đến một mảng làm đối số cho một hàm

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

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.

Go
Bài Viết Liên Quan:
Phương thức (method) trong Go
Trung Nguyen 02/12/2021
Phương thức (method) trong Go

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.

Struct trong Go
Trung Nguyen 28/11/2021
Struct 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 trong Go
Trung Nguyen 28/11/2021
Chuỗi trong Go

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.

Map trong Go
Trung Nguyen 28/11/2021
Map trong Go

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.