Bài 10 - Kiểu dữ liệu trong Go: Con trỏ


  • Trùm cuối

    Pointer

    Một nhóm kiểu dữ liệu quan trọng trong Go là kiểu dữ liệu tham chiếu, là kiểu dữ liệu mà bản thân nó không chứa dữ liệu thật sự cần xử lý mà chứa con trỏ tham chiếu đến địa chỉ vùng nhớ lưu trữ dữ liệu. Vậy con trỏ là gì?

    Con trỏ

    c1e76407-92c0-4c81-af5f-d6f24a157178-image.png

    Thông thường các biến lưu giữ các giá trị ứng với kiểu dữ liệu mà nó khai báo tại vùng nhớ gắn với nó. Ví dụ khi khai báo i := 10 thì chúng ta có biến i kiểu dữ liệu int chứa giá trị 10 tại vùng nhớ gắn với nó. Khi chúng ta gán lại i = 5 thì tại vùng nhớ của biến i lúc này sẽ lưu giá trị 5.

    Vùng nhớ gắn với biến hay còn gọi là địa chỉ của biến, đó là địa chỉ đầu vùng nhớ của biến. Thông tin địa chỉ của biến có ý nghĩa nhất định nên như các ngôn ngữ khác Go cung cấp toán tử & lấy địa chỉ của một biến. Nên khi chúng ta gọi &i sẽ cho giá trị 0xc082008340.

    Có được địa chỉ của biến i thì có thể truy xuất giá trị mà biến i lưu trữ và thậm chí có thể thay đổi giá trị này nên nhu cầu biến lưu giữ địa chỉ của một biến khác như biến i ở trên là có. Biến này Go gọi là biến con trỏ (pointer), tương tự như ở các ngôn ngữ khác.

    Biến con trỏ một kiểu dữ liệu sẽ được khai báo như sau: var <tên biến con trỏ> *<tên kiểu dữ liệu>.

    i := 10 
    fmt.Println(i)     //  10 
    fmt.Println(&i)    //   0xc082008340 
    var p1 *int 
    fmt.Println(p1)    //   nil 
    p1 = &i 
    p2 := &i 
    fmt.Println(p1)    //  0xc082008340 
    fmt.Println(p2)    //  0xc082008340 
    fmt.Println(&p1)   //  0xc082024020
    
    • Dòng 1 khai báo biến i kiểu int có giá trị 10 như ta thấy kết quả in ra ở dòng 2.
    • Dòng 3 in ra địa chỉ biến i.
    • Dòng 4 & 5 khai báo biến kiểu con trỏ int p1 và in giá trị nó ra. Kết quả là nil. Đây là giá trị zero mặc định mà Go gán cho biến con trỏ, ý nghĩa là không trỏ đến đâu hết.
    • Dòng 6 gán giá trị biến p1 là địa chỉ biến i.
    • Dòng 7 là một kiểu khai báo nhanh biến con trỏ p2.
    • Dòng 8 và 9 in giá trị 2 biến con trỏ p1 và p2 và kết quả như chúng ta thấy là bằng nhau vì cùng trỏ đến biến i.
    • Dòng 10 in địa chỉ biến con trỏ p1. Có thể diễn giải là tại địa chỉ biến p1 có giá trị 0xc082024020 lưu địa chỉ của biến i có giá trị là 0xc082008340.

    Để truy xuất giá trị mà một biến con trỏ trỏ đến chúng ta dùng toán tử *<biến con trỏ>. Ví dụ ở trên *p1 hay *p2 sẽ cho giá trị 10. *p1 tương đương biến i nên khi cần gán lại giá trị cho i, ta có thể dùng *p1 = 5. Lúc này giá trị biến i cũng sẽ là 5.

    fmt.Println(*p1)  // 10 
    *p1 = 5 
    fmt.Println(i)    // 5
    

    Do giá trị của biến con trỏ luôn là địa chỉ của biến kiểu dữ liệu khác nên kích thước luôn nhỏ (tùy theo kiến trúc máy: 16 bit, 32 bit hay 64 bit), tiện cho việc khi dùng là tham số của hàm thay vì phải truyền giá trị cồng kềnh như biến giá trị kiểu mảng hay cấu trúc. Bên cạnh đó khi truyền kiểu con trỏ cho hàm, chúng ta dễ dàng thay đổi giá trị của biến mà con trỏ trỏ đến. Chúng ta sẽ bàn kỹ hơn ở phần về hàm.

    Con trỏ có thể so sánh bằng (==) hoặc khác (!=). Thường dùng nhất khi kiểm tra coi 1 hàm có trả về lỗi hay không. Go không cung cấp toán tử ++ cho biến con trỏ như C/C++.

    Hàm new

    Trong ví dụ nêu trên, biến i còn được sử dụng nhưng trong một số tình huống chúng ta chỉ cần tạo ra biến con trỏ trỏ đến một biến kiểu dữ liệu nào đó rồi thao tác trên biến con trỏ mà không cần quan tâm đến biến chứa giá trị nữa. Lúc này việc tạo ra biến và đặt tên là không cần thiết. Để giải quyết vấn đề này, Go cung cấp hàm new. Nó sẽ giúp tạo ra một biến không tên của một kiểu dữ liệu nào đó qua cách khai báo new(<kiểu dữ liệu>), trả về con trỏ kiểu dữ liệu đó. Áp dụng vào ví dụ trên:

    var p1 = new(int) 
    fmt.Println(p1)    //  0xc082008340 
    fmt.Println(*p1)   //  0 
    *p1 = 10 
    fmt.Println(*p1)   //  10
    
    • Dòng 1 khai báo biến con trỏ p1 và gán giá trị trả về từ hàm new. Lúc này ta có biến p1 là biến con trỏ kiểu int trỏ đến 1 biến không đặt tên.
    • Dòng 2 và 3 in ra giá trị biến con trỏ lưu giữ và giá trị mà nó trỏ đến. Như thường lệ, Go khởi tạo mặc định giúp chúng ta nên ở dòng 3 cho giá trị 0.
    • Dòng 4 gán giá trị cho biến mà con trỏ p1 trỏ đến và dòng 5 in giá trị của biến được trỏ đến đó ra màn hình.

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

    Trong bài tới chúng ta sẽ tìm hiểu về 2 kiểu tham chiếu được dùng rất phổ biến trong Go là slice và map.

    Tóm tắt

    • Con trỏ:
      🗨biến lưu giữ địa chỉ chứa dữ liệu thay vì chứa dữ liệu. Kích thước vùng nhớ luôn cố định do không phụ thuộc kiểu dữ liệu.
      🗨 Khai báo: var <tên biến con trỏ> *<kiểu dữ liệu>
      🗨 Toán tử: *p để lấy giá trị biến con trỏ p trỏ đến và &iđể lấy địa chỉ vùng nhớ biến i.
      🗨 Biến con trỏ có thể so sánh == hoặc !=.
    • Hàm new:
      🗨 Tạo vùng nhớ của một kiểu dữ liệu trả về địa chỉ vùng nhớ đó.
      🗨 Sử dụng khi cần tạo biến con trỏ để thao tác không qua trung gian một biến dữ liệu.

  • Giám sát viên

    1. Trường hợp nào sử dụng con trỏ thay vì biến thông thường vậy ad ?
    2. Biến con trỏ có làm tăng tốc độ ứng dụng ko? Nếu có thì mình lúc nào cũng sử dụng sẽ tốt hơn ko ?

  • Trùm cuối

    @lyquocnam đã nói trong Bài 10 - Kiểu dữ liệu trong Go: Con trỏ:

    1. Trường hợp nào sử dụng con trỏ thay vì biến thông thường vậy ad ?
    2. Biến con trỏ có làm tăng tốc độ ứng dụng ko? Nếu có thì mình lúc nào cũng sử dụng sẽ tốt hơn ko ?
    1. Con trỏ là một biến tham chiếu, trường hợp như bác muốn gán lại giá trị vào chính tham số tham chiếu của hàm thì sử dụng con trỏ
    2. Biến con trỏ không làm tăng tốc độ ứng dụng, nó chỉ giúp tăng hiệu năng cho logic nếu áp dụng đúng trường hợp cần thiết và chỉ nên sử dụng khi cần thiết.

  • Giám sát viên

    vd thực tế: Gin

    router.GET("/user/:name", func(c *gin.Context) {
    		name := c.Param("name")
    		c.String(http.StatusOK, "Hello %s", name)
    	})
    

    c *gin.Context khác c gin.Context sao ad ? Mình chỉ hiểu nôm na rồi xài chứ thực sự vẫn còn lơ mơ :3


  • Trùm cuối

    @lyquocnam đã nói trong Bài 10 - Kiểu dữ liệu trong Go: Con trỏ:

    vd thực tế: Gin

    router.GET("/user/:name", func(c *gin.Context) {
    		name := c.Param("name")
    		c.String(http.StatusOK, "Hello %s", name)
    	})
    

    c *gin.Context khác c gin.Context sao ad ? Mình chỉ hiểu nôm na rồi xài chứ thực sự vẫn còn lơ mơ :3

    Biến con trỏ là biến chứa địa chỉ vùng nhớ trỏ đến biến đó, trong trường hợp trên thì gin.Context là một interface với method của nó là một pointer receiver

    Một ví dụ đơn giản cho bạn dễ hiểu hơn:

    package main
    
    import "fmt"
    
    func contro(t *int) { // Hàm này có tham số kiểu con trỏ nhận đầu vào là địa chỉ của biến
    	*t = 9 // Sử dụng * để truy cập giá trị của biến con trỏ và gán lại nó
    }
    
    func main() {
    	t := 6         // t là biến thường kiểu int
    	fmt.Println(t) // in ra chắc chắn bằng 6
    	contro(&t)     // Gọi hàm contro để gán lại giá trị cho t với tham số là địa chỉ của t
    	fmt.Println(t) // In ra lại đảm bảo khác 6
    }
    

  • Giám sát viên

    ok ad, đã hiểu vấn đề, thanks ad nhá



  • Bài viết hay ! Thanks ad 😀



Có thể bạn cũng quan tâm

.
DMCA.com Protection Status