& 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ênmap[string]*Tvới khóa không tồn tại, trả vềnil- bỏ qua lỗi:
f, _ := os.Open(path)đểflà 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 (*intphân biệt được "không có" với0)
Không dùng con trỏ khi:
- giá trị nhỏ và chỉ để đọc (
time.Time, mộtPoint) - 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*[]inthay*map[string]intgầ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.
&vtrỏ 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ầnappend. Nếuappendcấ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.