Menu

Phương thức trong Golang: receiver giá trị và con trỏ

Phương thức là một hàm có receiver. Tìm hiểu cách khai báo phương thức, khi nào dùng receiver giá trị hay receiver con trỏ, method set và interface, cùng phương thức trên các kiểu không phải struct.

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.

Khai báo phương thức

Phương thức (method) là một hàm có receiver: một tham số bổ sung được viết trước tên phương thức. Receiver gắn phương thức với một kiểu.

Tên receiver thường là một hoặc hai chữ cái lấy từ tên kiểu (c cho Circle). Go không dùng this hay self, và các hướng dẫn về phong cách code cũng khuyên không đặt những tên đó.

Vì Circle có phương thức String() string, nó thỏa mãn fmt.Stringer, và fmt.Println in ra kết quả của phương thức này thay vì {2}.

Receiver giá trị và receiver con trỏ

Receiver giá trị (c Circle) nhận một bản sao. Receiver con trỏ (c *Circle) nhận địa chỉ, nên thay đổi trên các trường được giữ lại.

IncByValue vẫn biên dịch được nhưng không tạo ra thay đổi nào nhìn thấy được, một bug kinh điển. Nếu phương thức cần sửa receiver, nó phải dùng receiver con trỏ.

Các lời gọi trên cũng cho thấy hai tiện lợi mà Go mang lại. c.Inc() trên một biến lấy địa chỉ được sẽ được viết lại thành (&c).Inc(), và p.Value() trên một con trỏ được viết lại thành (*p).Value(). Bạn hiếm khi phải viết & hay * chỉ để gọi một phương thức.

Dấu & tự động cần một thứ lấy địa chỉ được. Phần tử của map hay một composite literal thì không:

m := map[string]Counter{"a": {}}
m["a"].Inc()    // compile error: cannot call pointer method Inc on Counter
Counter{}.Inc() // same error: a composite literal is not addressable

Hãy lưu con trỏ trong map (map[string]*Counter), hoặc sao chép giá trị ra, sửa nó rồi ghi lại.

Chọn loại receiver nào

Dùng receiver con trỏ khiDùng receiver giá trị khi
Phương thức sửa receiverKiểu nhỏ và không bao giờ bị các phương thức của nó sửa (time.Time, một điểm tọa độ)
Struct lớn (sao chép tốn chi phí)Kiểu là map, func hoặc chan (vốn đã là tham chiếu)
Kiểu chứa sync.Mutex hoặc thứ tương tựBạn muốn giá trị hoạt động như kiểu nguyên thủy, sao chép thoải mái
Các phương thức khác của kiểu đã dùng con trỏ

Go FAQ và thư viện chuẩn theo một nguyên tắc chung: hãy nhất quán. Nếu có phương thức cần receiver con trỏ, hãy cho mọi phương thức dùng receiver con trỏ, để method set dễ đoán. go vet cảnh báo khi một struct chứa sync.Mutex bị sao chép qua receiver giá trị (passes lock by value).

Method set và interface

Sự khác biệt này quan trọng nhất với interface. Method set của T chứa các phương thức có receiver giá trị. Method set của *T chứa cả phương thức có receiver giá trị lẫn receiver con trỏ.

Viết Square{Side: 3} mà thiếu & trong slice đó sẽ không biên dịch được:

cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (method Area has pointer receiver)

Lý do: một interface có thể chứa bản sao của Square không lấy địa chỉ được, nên Go không thể lấy địa chỉ của nó để gọi phương thức con trỏ. Xem interface để biết phần còn lại.

Phương thức trên kiểu không phải struct

Mọi kiểu đặt tên được khai báo trong package của bạn đều có thể có phương thức, không chỉ struct.

Giới hạn: kiểu phải được định nghĩa trong cùng package với phương thức, và kiểu nền của nó không được là con trỏ hay interface. Bạn không thể thêm phương thức cho int, string hay time.Time từ package của mình. Cách làm của Go là bọc nó trong một kiểu đặt tên mới. String() trên kiểu số nguyên cũng là cách enum có đầu ra dễ đọc; xem enum với iota.

Method value và method expression

Một phương thức gắn với một receiver cụ thể là một giá trị hàm:

Method value có receiver giá trị sẽ sao chép receiver ngay khi được tạo, đó là lý do f vẫn in ra Ana. Với receiver con trỏ, nó sẽ lưu con trỏ và thấy được các thay đổi sau đó. Method value rất tiện cho callback: http.HandleFunc("/", srv.handleIndex).

Lỗi thường gặp

  • Receiver giá trị trên phương thức cần sửa dữ liệu. Vẫn biên dịch, nhưng âm thầm sửa một bản sao.
  • Trộn hai loại receiver. Khiến khó biết T hay *T thỏa mãn interface. Mỗi kiểu chỉ chọn một loại.
  • Gọi phương thức con trỏ trên con trỏ nil. Được phép, và phương thức chạy với receiver nil. Nó chỉ panic khi chạm vào một trường. Một số kiểu (như danh sách liên kết an toàn với nil) cố ý tận dụng điều này; phần lớn thì không lường trước.

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

Phương thức và hàm trong Go khác nhau thế nào?

Phương thức có một receiver, viết giữa func và tên: func (c Circle) Area() float64. Bạn gọi nó trên một giá trị (c.Area()), nó thuộc method set của kiểu đó, và đây là cách một kiểu thỏa mãn interface. Ngoài ra, nó hoạt động giống một hàm có đối số đầu tiên là receiver.

Khi nào nên dùng receiver con trỏ trong Go?

Dùng receiver con trỏ khi phương thức phải sửa receiver, khi struct đủ lớn để việc sao chép ở mỗi lần gọi trở nên lãng phí, hoặc khi kiểu chứa thứ không được phép sao chép, chẳng hạn sync.Mutex. Nếu có một phương thức của kiểu cần receiver con trỏ, lời khuyên thông thường là cho tất cả phương thức dùng receiver con trỏ để nhất quán.

Vì sao kiểu không implement được interface khi phương thức có receiver con trỏ?

Phương thức có receiver con trỏ thuộc method set của *T, không thuộc T. Vì vậy var s Shape = Square{} thất bại với Square does not implement Shape (method Area has pointer receiver). Hãy gán một con trỏ: var s Shape = &Square{}.

Có thể định nghĩa phương thức cho kiểu có sẵn trong Go không?

Không trực tiếp. Phương thức chỉ khai báo được trên các kiểu đặt tên được định nghĩa trong cùng package. Hãy định nghĩa kiểu riêng trước, type Celsius float64, rồi thêm phương thức cho nó. Bạn không thể thêm phương thức cho int, string hay một kiểu từ package khá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