Skip to content

Phương thức trong Go

Sự khác biệt giữa phương thức và hàm trong Go là phương thức có receiver, còn hàm thì không, và chỉ có kiểu tùy chỉnh mới có thể có phương thức. Trước hết xem một ví dụ.

go
type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

Trước hết khai báo một kiểu IntSlice, kiểu cơ sở của nó là []int, sau đó khai báo ba phương thức Get, SetLen, hình dáng của phương thức không khác biệt quá lớn so với hàm, chỉ nhiều hơn một đoạn nhỏ (i IntSlice). i chính là receiver, IntSlice là kiểu của receiver, receiver tương tự như this hoặc self trong các ngôn ngữ khác, chỉ là trong Go cần chỉ định rõ ràng.

go
func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

Việc sử dụng phương thức tương tự như gọi một phương thức thành viên của lớp, trước khai báo, sau khởi tạo, rồi gọi.

Receiver giá trị

Receiver cũng chia thành hai loại, receiver giá trị và receiver con trỏ, trước hết xem một ví dụ

go
type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // Đã sửa đổi, nhưng không gây ảnh hưởng gì
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

Sau khi chạy mã trên, sẽ phát hiện giá trị của myInt vẫn là 1, không được sửa đổi thành 2. Khi phương thức được gọi, sẽ truyền giá trị của receiver vào phương thức, receiver trong ví dụ trên là một receiver giá trị, có thể đơn giản xem như một tham số hình thức, và việc sửa đổi giá trị của tham số hình thức sẽ không gây ảnh hưởng gì cho giá trị bên ngoài phương thức, vậy nếu gọi thông qua con trỏ thì sao?

go
func main() {
  myInt := MyInt(1)
  (&myInt).Set(2)
  fmt.Println(myInt)
}

Đáng tiếc là, mã như vậy vẫn không thể sửa đổi giá trị bên trong, để có thể khớp với kiểu của receiver, Go sẽ giải tham chiếu nó, giải thích là (*(&myInt)).Set(2).

Receiver con trỏ

Chỉ cần sửa đổi một chút, là có thể bình thường sửa đổi giá trị của myInt.

go
type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

Hiện tại receiver là một receiver con trỏ, tuy myInt là một kiểu giá trị, khi gọi phương thức receiver con trỏ thông qua kiểu giá trị, Go sẽ giải thích nó là (&myint).Set(2). Nên khi receiver của phương thức là con trỏ, bất kể người gọi có phải là con trỏ hay không, đều có thể sửa đổi giá trị bên trong.

Trong quá trình truyền tham số hàm, là sao chép giá trị, nếu truyền là một số nguyên, thì sao chép số nguyên đó, nếu là một slice, thì sao chép slice đó, nhưng nếu là một con trỏ, thì chỉ cần sao chép con trỏ đó, rõ ràng việc truyền một con trỏ tiêu hao tài nguyên ít hơn so với truyền một slice, receiver cũng không ngoại lệ, receiver giá trị và receiver con trỏ cũng cùng một đạo lý. Trong hầu hết các trường hợp, đều khuyến nghị sử dụng receiver con trỏ, nhưng hai loại này không nên trộn lẫn sử dụng, hoặc là đều dùng, hoặc là đều không dùng, xem một ví dụ dưới đây.

TIP

Cần tìm hiểu trước Interface

go
type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} Cách đúng
   an.Run()
}

Đoạn mã này sẽ không thể biên dịch, trình biên dịch sẽ xuất ra lỗi như sau

cannot use Dog{} (value of type Dog) as type Animal in assignment:
  Dog does not implement Animal (Run method has pointer receiver)

Dịch ra là, không thể sử dụng Dog{} để khởi tạo biến kiểu Animal, vì Dog không thực hiện Animal, có hai cách giải quyết, một là sửa receiver con trỏ thành receiver giá trị, hai là sửa Dog{} thành &Dog{}, tiếp theo lần lượt giải thích.

go
type Dog struct {
}

func (d Dog) Run() { // Sửa thành receiver giá trị
   fmt.Println("Run")
}

func main() { // Có thể chạy bình thường
   var an Animal
   an = Dog{}
   // an = &Dog{} Cũng có thể
   an.Run()
}

Trong mã ban đầu, receiver của phương thức Run*Dog, nên tự nhiên việc thực hiện interface Animal là con trỏ Dog, chứ không phải struct Dog, đây là hai kiểu khác nhau, nên trình biên dịch sẽ cho rằng Dog{} không phải là việc thực hiện của Animal, nên không thể gán cho biến an, nên cách giải quyết thứ hai là gán con trỏ Dog cho biến an. Tuy nhiên khi sử dụng receiver giá trị, con trỏ Dog vẫn có thể bình thường gán cho animal, đây là vì Go sẽ giải tham chiếu con trỏ trong trường hợp thích hợp, vì thông qua con trỏ có thể tìm thấy struct Dog, nhưng ngược lại thì không thể, không thể thông qua struct Dog để tìm con trỏ Dog. Nếu đơn thuần trộn lẫn receiver giá trị và receiver con trỏ trong struct thì không sao, nhưng sau khi sử dụng cùng với interface, sẽ xuất hiện lỗi, chi bằng bất kể khi nào hoặc là đều dùng receiver giá trị, hoặc là đều dùng receiver con trỏ, hình thành một quy phạm tốt, cũng có thể giảm gánh nặng bảo trì sau này.

Còn một trường hợp nữa, là khi receiver giá trị có thể tìm địa chỉ, Go sẽ tự động chèn toán tử con trỏ để gọi, ví dụ slice là có thể tìm địa chỉ, vẫn có thể thông qua receiver giá trị để sửa đổi giá trị bên trong của nó. Ví dụ như đoạn mã dưới đây

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func main() {
  s := make(Slice, 1)
  s.Set(0, 1)
  fmt.Println(s)
}

Xuất ra

[1]

Nhưng như vậy sẽ gây ra một vấn đề khác, nếu thêm phần tử vào nó, tình huống lại khác. Xem ví dụ dưới đây

type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func (s Slice) Append(a int) {
  s = append(s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}
[1]

Xuất ra của nó vẫn như trước, hàm append có giá trị trả về, sau khi thêm phần tử vào slice phải ghi đè slice ban đầu, đặc biệt là sau khi mở rộng, việc sửa đổi receiver giá trị trong phương thức sẽ không gây ảnh hưởng gì, điều này cũng dẫn đến kết quả trong ví dụ, sửa thành receiver con trỏ là bình thường.

go
type Slice []int

func (s *Slice) Set(i int, v int) {
  (*s)[i] = v
}

func (s *Slice) Append(a int) {
  *s = append(*s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}

Xuất ra

[1 2]

Golang by www.golangdev.cn edit