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ụ.
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, Set và Len, 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.
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ụ
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?
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.
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
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.
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 là *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
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.
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]