Menu

Con trỏ trong Golang: & và *, new, nil và khi nào nên dùng

Con trỏ lưu địa chỉ của một giá trị. Tìm hiểu & và *, new, con trỏ tới struct, vì sao trả về con trỏ tới biến cục bộ là an toàn, khi nào nên dùng con trỏ, và panic khi giải tham chiếu con trỏ nil.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

& và *

Con trỏ (pointer) lưu địa chỉ bộ nhớ của một giá trị. &x cho bạn một con trỏ tới x. *p đi theo con trỏ tới giá trị.

Dấu sao có hai vai trò. Trong một kiểu (*int), nó nghĩa là "con trỏ tới int". Trong một biểu thức (*p), nó nghĩa là "giá trị mà p trỏ tới". In chính p ra sẽ thấy một địa chỉ như 0xc000012345, khác nhau giữa các lần chạy.

Vì sao cần con trỏ: dùng chung thay vì sao chép

Go truyền mọi thứ theo giá trị. Hàm nhận một bản sao của từng đối số, nên không thể thay đổi biến của nơi gọi. Truyền con trỏ thì sao chép địa chỉ, và cả hai phía cùng chạm tới một giá trị.

Go không có tham chiếu theo nghĩa của C++. "Truyền tham chiếu" trong Go nghĩa là truyền một con trỏ theo giá trị.

Con trỏ tới struct

Con trỏ tới struct là loại con trỏ phổ biến nhất trong code Go. Truy cập trường sẽ tự động giải tham chiếu: p.Name nghĩa là (*p).Name.

Phương thức cũng theo logic đó: receiver con trỏ cho phép phương thức thay đổi struct. Trang phương thức trình bày cách chọn giữa receiver giá trị và receiver con trỏ.

new

new(T) cấp phát một T mang giá trị zero và trả về địa chỉ của nó. Điều này giống hệt việc khai báo một biến rồi lấy địa chỉ.

p := new(int) // *int pointing at 0
var x int
q := &x           // same thing, two lines
a := new(Account) // same as &Account{}

Trong thực tế, &T{...} phổ biến hơn với struct vì nó cho phép gán trường ngay trong cùng biểu thức. new tiện cho con trỏ tới các kiểu cơ bản. Đừng nhầm nó với make, hàm tạo slice, map và channel rồi trả về chúng ở trạng thái dùng được ngay, không phải dưới dạng con trỏ.

Trả về con trỏ tới biến cục bộ là an toàn

Trong C, trả về địa chỉ của biến cục bộ là bug. Trong Go, đó là code bình thường.

Phân tích thoát (escape analysis) của trình biên dịch quyết định một giá trị nằm trên stack hay heap. Bạn không bao giờ phải chọn, cũng không bao giờ phải giải phóng bộ nhớ: bộ thu gom rác lấy lại vùng nhớ khi không còn gì trỏ tới nó. go build -gcflags=-m in ra các quyết định đó nếu bạn tò mò.

Con trỏ nil

Giá trị zero của mọi kiểu con trỏ là nil. Giải tham chiếu nil sẽ panic.

Đầu ra:

1
2
true
recovered: runtime error: invalid memory address or nil pointer dereference

Nếu không có recover, chương trình sẽ sập với thông báo đó và một stack trace bắt đầu bằng panic: runtime error: invalid memory address or nil pointer dereference và [signal SIGSEGV: segmentation violation ...]. Các nguồn gây lỗi thường gặp:

  • một biến con trỏ hoặc một trường của struct chưa bao giờ được gán
  • m[key] trên map[string]*T với khóa không tồn tại, trả về nil
  • bỏ qua lỗi: f, _ := os.Open(path) để f là nil khi thất bại
  • một phương thức có receiver con trỏ được gọi trên con trỏ nil, rồi đọc một trường

Bản thân việc gọi phương thức trên con trỏ nil là hợp lệ. Panic chỉ xảy ra khi phương thức chạm vào các trường của receiver.

Khi nào nên dùng con trỏ

Dùng con trỏ khi:

  • một hàm hoặc phương thức phải thay đổi giá trị của nơi gọi
  • giá trị là một struct lớn và được truyền đi nhiều lần
  • kiểu không được phép sao chép (nó chứa sync.Mutex, hoặc đại diện cho một tài nguyên duy nhất như một kết nối)
  • bạn cần biểu diễn "không có giá trị" bằng nil, ví dụ một trường tùy chọn trong struct JSON (*int phân biệt được "không có" với 0)

Không dùng con trỏ khi:

  • giá trị nhỏ và chỉ để đọc (time.Time, một Point)
  • kiểu vốn đã hoạt động giống tham chiếu: slice, map, channel, hàm và interface hiếm khi cần * phía trước. Một *[]int hay *map[string]int gần như luôn là sai lầm

Con trỏ không tự động nhanh hơn. Sao chép một struct nhỏ rất rẻ, còn con trỏ có thể buộc cấp phát trên heap và tạo thêm việc cho bộ thu gom rác.

Con trỏ và biến vòng lặp

Lấy địa chỉ của biến vòng lặp là an toàn từ Go 1.22, vì mỗi lần lặp có biến riêng:

Đoạn này in ra 0 1 2. Trước Go 1.22 (bản cũng giới thiệu range 3), cùng vòng lặp đó viết dưới dạng for i := 0; i < 3; i++ in ra 3 3 3, vì cả ba con trỏ dùng chung một biến. Lưu ý rằng &v trong for _, v := range items trỏ tới một bản sao của phần tử, không phải phần tử trong slice. Để lấy con trỏ tới chính phần tử, hãy dùng &items[i].

Lỗi thường gặp

  • Giải tham chiếu mà không kiểm tra. Kiểm tra if p == nil ở mọi chỗ có thể là nil.
  • Con trỏ tới giá trị của range. &v trỏ tới một bản sao; hãy dùng &s[i].
  • Con trỏ tới slice và map. Hiếm khi cần. Hãy trả về slice mới.
  • Giữ &s[i] qua một lần append. Nếu append cấp phát lại, con trỏ vẫn trỏ tới mảng cũ và các lần ghi sau đó không hiện ra ở đâu cả.

Câu hỏi thường gặp

& và * có nghĩa là gì trong Go?

&x lấy địa chỉ của x và cho ra một con trỏ kiểu *T. *p giải tham chiếu con trỏ: nó đọc hoặc ghi giá trị mà con trỏ trỏ tới. Trong một kiểu, *T nghĩa là "con trỏ tới T".

new và make trong Go khác nhau thế nào?

new(T) cấp phát một T mang giá trị zero và trả về *T; nó dùng được với mọi kiểu. make chỉ dùng cho slice, map và channel, và trả về một giá trị đã được khởi tạo (không phải giá trị zero, không phải con trỏ) của kiểu đó: make(map[string]int) dùng được ngay, còn new(map[string]int) là một con trỏ tới map nil.

Có thể trả về con trỏ tới biến cục bộ trong Go không?

Có. Khác với C, việc này an toàn. Phân tích thoát (escape analysis) của trình biên dịch thấy biến sống lâu hơn hàm và cấp phát nó trên heap. func newInt() *int { x := 5; return &x } là code Go đúng.

Nguyên nhân gây lỗi "invalid memory address or nil pointer dereference" trong Go là gì?

Đọc hoặc ghi qua một con trỏ nil: một biến *T chưa bao giờ được gán, tra cứu một khóa không tồn tại trong map[K]*V, hoặc một hàm trả về nil kèm một lỗi mà bạn không kiểm tra. Hãy kiểm tra nil, hoặc khởi tạo con trỏ trước khi dùng.

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

Không. Bạn không thể cộng thêm vào con trỏ hay truy cập bộ nhớ theo chỉ số qua nó. Package unsafe cho phép điều đó với code cấp thấp, không có bảo đảm an toàn nào. Code Go thông thường dùng slice cho vùng nhớ liên tục.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu