Tìm hiểu về sử dụng con trỏ trong ngôn ngữ Go

Con trỏ là gì ?

    Chắc hẳn ai học C/C++ cũng đã nghe qua khái niệm con trỏ. Con trỏ trong Go cũng giống như trong C/C++ là một loại biến đặc biệt được được dùng để lưu trữ địa chỉ của biến khác trong bộ nhớ RAM.

    

    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 và gán địa chỉ con trỏ trong golang

Khai báo

    Con trỏ có kiểu dữ liệu *int nếu nó trỏ đến địa chỉ bộ nhớ của dữ liệu kiểu int, và nó có kiểu dữ liệu *string nếu nó trỏ đến địa chỉ bộ nhớ của dữ liệu kiểu string.

    Cú pháp để tạo và định nghĩa con trỏ là var p *Type trong đó Type là kiểu dữ liệu của dữ liệu mà nó trỏ tới

    Ví dụ:

package main

import "fmt"

func main() {
	var p *int

	fmt.Println("p = ", p)
}

    Đoạn code khi chạy sẽ cho kết quả là: p = <nil> do chưa gán địa chỉ cho con trỏ.

    Ngoài cách khai báo trên, Go còn hỗ trợ 1 kiểu khai báo khác new(<kiểu dữ liệu>). Nó sẽ giúp tạo ra 1 biến không tên của kiểu dữ liệu và trả về con trỏ của kiểu dữ liệu đó.

package main

import "fmt"

func main() {
  p := new(int)
  
  fmt.Println(p)
  fmt.Println(*p)
}

    Kết quả:

0xc000016108
0

    Khác so với khai bảo ở trên là kiểu new không cần 1 giá trị để con trỏ p trỏ tới mà Go sẽ khởi tạo mặc định 1 giá trị

  • với kiểu int là 0
  • với kiểu string là "" (string rỗng)
  • với kiểu bool là false ...

    và các giá trị mặc định này cũng có địa chỉ cho nó.

    Hàm new này không được sử dụng nhiều và nó cũng không phải từ khóa nên chúng ta có quyền đặt tên biến và hàm trùng với nó. Lúc này, hàm new do Go cung cấp sẽ mất tác dụng

Gán địa chỉ

    Để gán địa chỉ cho con trỏ ta có thể xem ví dụ sau

package main

import "fmt"

func main() {
	a := 1 
	p := &a

	fmt.Println("Value stored in variable a = ", a)
	fmt.Println("Address of variable a = ", &a)
	fmt.Println("Value stored in variable p = ", p)
}

    Đoạn code trên khai báo biến a mang giá trị 1. &a sẽ lấy ra địa chỉ của biến a. p := &a sẽ khai báo và gán giá trị địa chỉ của biến a cho p. p sẽ là con trỏ lưu địa chỉ biến a

    Đoạn code trên sẽ cho kết quả:

Value stored in variable a =  1
Address of variable a =  0xc000016108
Value stored in variable p =  0xc000016108

    Ngoài ra ta có thay đổi giá trị được lưu trữ trong biến thông qua con chỏ bằng cách tham chiếu ngược đến con trỏ

    Vi dụ:

package main

import "fmt"

func main() {
	a := 1
	p := &a

	fmt.Println("a = ", a)
	fmt.Println("p = ", p)
    fmt.Println("*p = ", *p)

	*p = 2
	fmt.Println("a (after) = ", a)
}

    Đoạn code trên sẽ cho kết quả:

a =  1
p =  0xc000016108
*p =  1
a (after) =  2

    Trong ví dụ trên ta có thể thấy p đang lưu địa chỉ của biến a, ta có thể lấy giá trị biến a thông qua con trỏ p bằng cách sử dụng *p và cũng có thể sử dụng *p để thay đổi giá trị của biến a.

    Đầu tiên ta gán giá trị a = 1, sau đó khai báo con trỏ p lưu địa chỉ của biến a (p := &a)

    fmt.Println("a = ", a) : hiển thị giá trị biến a (1)

    fmt.Println("p = ", p) : hiển thị giá trị con trỏ p, lúc này p đang lưu địa chỉ của biến a nên p có giá trị 0xc000016108

    fmt.Println("*p = ", *p) : hiển thị giá trị lưu tại địa chỉ p(0xc000016108) , vì biến a lưu trữ trên địa chỉ này nên *p hiển thị giá trị của biến a (1)

    *p = 2: thay đổi giá trị trên địa chỉ p(0xc000016108) lên 2 , lúc trước giá trị bằng 1.

    vì giá trị lưu ở địa chỉ 0xc000016108 đã thay đổi từ 1 thành 2 nên lúc này biến a có giá trị 2

    fmt.Println("a (after) = ", a) : kết quả là 2

Con trỏ trỏ tới con trỏ

    Liệu có thể sử dụng con trỏ để lưu địa chỉ của 1 con trỏ, Cùng xem ví dụ dưới đây

package main

import "fmt"

func main() {
	a := 1
	p := &a
	pp := &p

	fmt.Println("a = ", a)
	fmt.Println("address of a = ", &a)

	fmt.Println("p = ", p)
	fmt.Println("address of p = ", &p)

	fmt.Println("pp = ", pp)

	fmt.Println("*pp = ", *pp)
	fmt.Println("**pp = ", **pp)
}

    Kết quả của chương trình

a =  1
address of a =  0xc000016108
p =  0xc000016108
address of p =  0xc00000e028
pp =  0xc00000e028
*pp =  0xc000016108
**pp =  1

    a := 1 gán giá trị cho biến a bằng 1

    p := &a khởi tạo con trỏ p lưu địa chỉ của biến a (có địa chỉ là 0xc000016108)

    pp := &p khởi tạo con trỏ pp lưu địa chỉ của con trỏ p (có địa chỉ là 0xc00000e028)

    Các dòng tiếp theo hiên thị thông tin

  • a : giá trị của biến a
  • &a : địa chỉ lưu biến a
  • p : giá trị của p, lúc này giá trị của p là địa chỉ của a
  • &p : địa chỉ của con trỏ p
  • pp: lúc này pp lưu địa chỉ của con trỏ p (0xc00000e028)
  • *pp : giá trị của cái địa chỉ lưu trên pp -> chính là cái địa chỉ lưu trên p (0xc000016108) => là địa chỉ của biến a
  • ** pp: giá trị của giá trị của cái địa chỉ lưu trên pp => là gía trị của a = 1

Phép toán trên con trỏ

    Trong C/C++ ta có thể sử dụng các phép toán trên con trỏ. Ví dụ như

int a=1;

int *p;

p = &a;

pa ++;

    Giả sử biến a được lưu trữ tại địa chỉ 1000 => p lưu giá trị 1000. Vì số nguyên có kích thước là 2 bytes, nên sau biểu thức “p++;” p sẽ có giá trị là 1002 mà không là 1001.

    Vậy trong Go điều tường tự có xảy ra. Hãy cũng xem ví dụ sau:

package main

import "fmt"

func main() {
	a := 1
	p := &a
    p++

	fmt.Println(p)
}

    Khi chạy đoạn code này ta sẽ thấy thông báo lỗi:

# command-line-arguments
./main.go:8:4: invalid operation: p++ (non-numeric type *int)

    Điều này là do Go không hỗ trợ các phép toán trên con trỏ. Vậy nên các bạn không cần phải thử các phép toán khác trên con trỏ giống như C/C++ nhé.

Ứng dụng con trỏ trong Golang

    Ta cùng xem 1 ví dụ sau:

package main

import "fmt"

func main() {

  a := 1
  updateByValue(a)
  fmt.Println(a)

  updateByPointer(&a)
  fmt.Println(a)

}

func updateByValue(a int) {
  a = 2
}
func updateByPointer(a *int){
  *a = 2
}

    Hàm updateByValue() truyền vào giá trị biến a và thay đổi biến a thành 2 Hàm updateByPointer() truyền vào con trỏ biến a và thay đổi biến a thành 2

    Ta có thể thấy hàm updateByValue không thể thay đổi giá trị của biến a thành 2. Thực chất là do ta chỉ truyền vào "bản sao" giá trị của biến a vào nên ta không thể làm thay đổi được a. Khi dùng hàm updateByPointer() thì giá trị của biến a có thể thay đổi vì ta truyền địa chỉ biến a vào và có thể thay đổi trực tiếp giá trị trên địa chỉ đó.

    Ta cùng xem 1 ví dụ khác:

package main

import "fmt"

func main() {

  a := []int{1, 2}
  updateInValue(a)
  fmt.Println(a)
}

func updateInValue(a []int) {
  a[0] = 2
}

    Trong ví dụ trên ta dùng hàm updateInValue để thay đổi giá trị của phần tử đầu tiên của slices và hàm này có thể thay đổi được giá trị.

    Do đó tùy từng trường hợp mà ta mới cần phải sử dụng con trỏ. Trong Go các kiểu dữ liệu có thể chia thành 2 loại Value Types và Reference Types. Đối với kiểu Value Types cần phải để sử dụng con trỏ khi thay đổi một cái gì đó trong function còn đối với kiểu Reference Types thì không cần phải lo tới con trỏ khi làm điều trên.

Tham Khảo:

    https://medium.com/rungo/pointers-in-go-a789eafccd53

Nguồn: Viblo

Bình luận
Vui lòng đăng nhập để bình luận
Một số bài viết liên quan