Bài 8 đã nói receiver giá trị không sửa được đối tượng. Bài này về hệ quả thứ hai, tinh vi hơn và chỉ lộ ra khi bạn dùng interface.

Method nằm ngoài kiểu

type Cho struct{ Ten string }

func (c Cho) Noi() string { return c.Ten + ": gâu" }

Method khai bên ngoài struct, không nằm trong dấu ngoặc nhọn như Java. Điều này cho phép:

Gắn method vào bất kỳ kiểu nào bạn định nghĩa, kể cả kiểu không phải struct:

type Nhiet float64
func (n Nhiet) String() string { return fmt.Sprintf("%.1f°C", float64(n)) }

Nhưng không gắn được vào kiểu của package khác. func (s string) Hoa() string là lỗi. Muốn thêm method cho time.Time thì phải định nghĩa kiểu riêng bọc nó lại. Đây là ràng buộc cố ý — nó ngăn hai package cùng thêm method trùng tên vào một kiểu.

Method set: khái niệm quyết định

Đây là phần chính của bài.

Method set của kiểu T gồm mọi method có receiver T. Method set của kiểu *T gồm mọi method có receiver T *T.

Con trỏ có nhiều method hơn giá trị. Và điều này chỉ quan trọng ở đúng một chỗ: gán vào interface.

type Noi interface{ Noi() string }

type Meo struct{ Ten string }
func (m *Meo) Noi() string { ... }   // receiver CON TRỎ
  var n Noi = &Meo{"Mimi"}   -> OK
  var n Noi = Meo{"Mimi"}    -> LỖI BIÊN DỊCH

Thông báo lỗi của Go rất rõ ràng:

  cannot use Meo{} (value of type Meo) as Noi value:
  Meo does not implement Noi (method Noi has pointer receiver)

Vì sao lại thế? Bài 8 đã đo: Go tự chèn & khi bạn gọi b.ThemConTro(1) trên một biến giá trị. Nhưng nó chỉ làm được vậy khi biến có địa chỉ. Một giá trị nằm trong interface thì không — nó là bản sao trong một ô nhớ mà bạn không lấy địa chỉ được.

Hệ quả thực tế

Bạn sẽ gặp lỗi này nhiều nhất ở ba chỗ:

Truyền vào hàm nhận interface:

func XuLy(n Noi) { }
XuLy(Meo{})     // lỗi
XuLy(&Meo{})    // đúng

Slice của interface:

ds := []Noi{Cho{"Milu"}, &Meo{"Mimi"}}   // Cho dùng receiver giá trị nên OK

Phần tử của map: như bài 8 đã nói, m["a"].MethodConTro() là lỗi vì phần tử map không có địa chỉ.

Quy tắc chọn receiver

Từ bài 8 và bài này, gộp thành một quy tắc thực dụng:

Dùng receiver con trỏ làm mặc định cho kiểu bạn định nghĩa, trừ khi có lý do rõ ràng để dùng giá trị.

Lý do: nó tránh cả hai bẫy — method sửa được đối tượng, và kiểu thoả mãn interface theo cách bạn mong đợi khi truyền con trỏ.

Dùng receiver giá trị khi kiểu là bất biến và nhỏ — time.Time, Nhiet ở trên, các kiểu giá trị kiểu Diem{X, Y int}.

Đừng trộn lẫn trên cùng một kiểu. Nếu một method cần con trỏ thì cho tất cả dùng con trỏ. Trộn lẫn làm method set khó đoán và go vet sẽ nhắc bạn.

String() và các interface ngầm

func (n Nhiet) String() string { return fmt.Sprintf("%.1f°C", float64(n)) }

Định nghĩa method String() string là kiểu của bạn tự động thoả mãn fmt.Stringer, và fmt.Println sẽ gọi nó. Không cần khai gì — đó là chủ đề của bài mai.

Cẩn thận một cái bẫy: nếu String() của bạn gọi fmt.Sprintf("%v", n) với n là chính kiểu đó, bạn có đệ quy vô hạn và tràn ngăn xếp. Phải ép về kiểu nền trước, như float64(n) ở trên.

Thử ba mươi giây

type T struct{}
func (t *T) M() {}

var _ interface{ M() } = T{}    // lỗi
var _ interface{ M() } = &T{}   // OK

Đổi *T thành T trong khai báo method rồi thử lại — lúc đó cả hai dòng đều chạy, vì method set của *T luôn chứa method của T.

Ba mươi giây đó là toàn bộ quy tắc method set, và nó giải thích 90% các lỗi "does not implement" bạn sẽ gặp.

Ngày mai: interface — thoả mãn ngầm định, và vì sao đó là ý tưởng hay nhất của Go.