Chào mừng đến với bài hướng dẫn số 15 trong series Hướng dẫn lập trình Golang cơ bản. Trong bài này chúng ta sẽ nói về con trỏ - Pointer.

Con trỏ là gì?

Một con trỏ là một biến chứa địa chỉ bộ nhớ của biến khác.

pointer

Nhìn vào hình trên, biến b có giá trị 156 và được lưu tại địa chỉ 0x1040a124. Biến a lưu địa chỉ của biến b, và được gọi là trỏ đến b.

Khai báo con trỏ

*T là kiểu biến con trỏ mà trỏ đến một giá trị của kiểu T.

Chúng ta hãy thử code:

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)
}

Run in playground

Toán tử & được sử dụng để lấy địa chỉ của một biến. Tại dòng 9 của chương trình trên, chúng ta đang gán địa chỉ của biến b cho a có kiểu *int. Như vậy có thể nói a được trỏ đến b. Khi in giá trị của a, địa chỉ của b sẽ được in ra như sau:

Type of a is *int  
address of b is 0x1040a124

Do giá trị của b có thể là bất cứ đâu trong bộ nhớ, mỗi lần truy cập các bạn có thể có một địa chỉ khác của biến b.

Giá trị ZERO của con trỏ

Giá trị zero 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)
    }
}

Run in playground

Trong ví dụ trên, biến b được khởi tạo là nil, sau đó được gán vào địa chỉ của biến a. Ví dụ trên sẽ xuất ra:

b is <nil>  
b after initialisation is 0x1040a124

Tham chiếu ngược (dereferencing a pointer)

Tham chiếu ngược một con trỏ nghĩa là truy cập vào giá trị của biến mà con trỏ trỏ tới. *a là cú pháp tham chiếu ngược đến a.

Hãy xem thử ví dụ dưới đây:

package main  
import (  
    "fmt"
)

func main() {  
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)
}

Run in playground

Tại dòng 10 ví dụ trên, chúng ta tham chiếu ngược a và in ra giá trị của nó. Thật vậy, chương trình sẽ in ra giá trị của b:

address of b is 0x1040a124  
value of b is 255 

Chúng ta thử viết một ví dụ khác trong đó thay đổi giá trị của biến b 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)
}

Run in playground

Tại dòng 12 của ví dụ trên, chúng ta tăng giá trị được trỏ đến bởi biến a bằng 1, điều này sẽ thay đổi giá trị của biến b do a trỏ đến b. Do đó giá trị mới của b sẽ là 256, chương trình sẽ xuất ra:

address of b is 0x1040a124  
value of b is 255  
new value of b is 256

Sử dụng con trỏ trong 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)
}

Run in playground

Trong ví dụ trên, tại dòng 14 chúng ta đưa vào con trỏ biến b lưu địa chỉ của a vào hàm change. Trong hàm change, giá trị của a bị thay đổi bằng cách tham chiếu ngược trong dòng 8. Chương trình xuất ra:

value of a before function call is 58  
value of a after function call is 55 

Hãy sử dụng con trỏ cho slice thay vì array trong hàm

Giả định rằng chúng ta cần sửa đổi một array trong hàm và những thay đổi đó hiển thị với caller. Để làm điều này, ta sử dụng con trỏ cho array như một đối số trong 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)
}

Run in playground

Tại dòng 13 ví dụ trên, chúng ta đưa vào địa chỉ của array a trong hàm modify. Tại dòng 8, chúng ta tham chiếu ngược arr và gán 90 cho thành phần đầu tiên của array. Chương trình sẽ xuất ra 

[90 90 91]

a[x] là cách viết gọn của (*a)[x]. Nên (*a)[0] trong ví dụ trên có thể được thay thế bởi arr[0]. Hãy thử viết lại ví dụ trên bằng cú pháp ngắn gọn:

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)
}

Run in playground

Chương trình trên sẽ xuất ra:

[90 90 91]

Mặc dù cách này sử dụng con trỏ cho array như một đối số trong hàm và thực hiện thay đổi nó, tuy nhiên vẫn không được coi là phương pháp "chuẩn" trong Go. Chúng ta hãy dùng slice thay vì array.

Thử viết lại chương trình trên 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)
}

Run in playground

Tại dòng 13 ví dụ trên, chúng ta đưa vào một slice trong hàm modify. Phần tử đầu tiên của slice được sửa thành 90 trong hàm modify. Chương trình xuất ra:

[90 90 91]

Do vậy, hãy quên con trỏ cho array, thay vào đó sử dụng slice, đoạn code sẽ tường minh hơn và là "chuẩn mực" trong Go :).

Go không hỗ trợ phép toán số học trên con trỏ

Go không hỗ trợ phép toán số học trên con trỏ giống nhiều ngôn ngữ lập trình khác như C.

Xét ví dụ dưới đây:

package main

func main() {  
    b := [...]int{109, 110, 111}
    p := &b
    p++
}

Run in playground

Chương trình sẽ trả về lỗi 

main.go:6: invalid operation: p++ (non-numeric type *[3]int)

Phần hướng dẫn về con trỏ đến đây là kết thúc. Các bạn có thể truy cập github để xem các ví dụ mẫu.

Xin vui lòng để lại những ý kiến và phản hồi của các bạn. Chúc một ngày tốt lành!

Hẹn gặp lại trong bài học tiếp theo: Structures.

 

Bài viết được dịch từ trang https://golangbot.com.