Trong hướng dẫn này, chúng ta sẽ tìm hiểu phương thức (method) trong Go là gì? Cú pháp khai báo phương thức, so sánh phương thức với hàm, ... trong Go.
Một phương thức (method) chỉ là một hàm có kiểu bộ thu (receiver type) đặc biệt giữa từ khóa func
và tên phương thức. Bộ thu (receiver) có thể là kiểu struct hoặc kiểu non-struct.
Cú pháp của khai báo phương thức được cung cấp bên dưới.
func (t Type) methodName(parameter list) {
}
Đoạn mã trên tạo một phương thức có tên methodName
với kiểu bộ thu là Type
. t
được gọi là bộ thu và nó có thể được truy cập trong phương thức.
Hãy viết một chương trình đơn giản tạo một phương thức trên kiểu struct và gọi nó.
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() method has Employee as the receiver type
*/
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee {
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary() //Calling displaySalary() method of Employee type
}
Chạy chương trình trong playground
Tại dòng số 16 của chương trình trên, chúng ta đã tạo một phương thức displaySalary
có kiểu bộ thu là struct Employee
. Phương thức displaySalary()
có quyền truy cập vào bộ thu e
bên trong nó.
Tại dòng số 17, chúng ta đang sử dụng bộ thu e
và in tên, đơn vị tiền tệ và mức lương của nhân viên.
Tại dòng số 26 chúng ta đã gọi phương thức bằng cú pháp emp1.displaySalary()
.
Chương trình này in kết quả:
Salary of Sam Adolf is $5000
Chương trình trên có thể được viết lại chỉ bằng cách dùng hàm và không cần phương thức.
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() method converted to function with Employee as parameter
*/
func displaySalary(e Employee) {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee{
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
displaySalary(emp1)
}
Chạy chương trình trong playground
Trong chương trình trên, phương thức displaySalary
được chuyển đổi thành một hàm và struct Employee
được truyền dưới dạng một tham số cho nó. Chương trình này cũng tạo ra cùng một đầu ra:
Salary of Sam Adolf is $5000
Vậy tại sao chúng ta lại có phương thức khi chúng ta hoàn toàn có thể viết cùng một chương trình bằng cách sử dụng hàm. Có một vài lý do cho việc này. Hãy xem xét từng cái một.
Employee
có thể được nhóm lại bằng cách tạo các phương thức sử dụng kiểu bộ thu Employee
. Ví dụ, chúng ta có thể thêm các phương thức như calculatePension
, calculateLeaves
v.v.Square
và Circle
. Có thể định nghĩa một phương thức có tên Area
trên cả Square
và Circle
. Điều này được thực hiện trong chương trình dưới đây.package main
import (
"fmt"
"math"
)
type Rectangle struct {
length int
width int
}
type Circle struct {
radius float64
}
func (r Rectangle) Area() int {
return r.length * r.width
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func main() {
r := Rectangle{
length: 10,
width: 5,
}
fmt.Printf("Area of rectangle %d\n", r.Area())
c := Circle{
radius: 12,
}
fmt.Printf("Area of circle %f", c.Area())
}
Chạy chương trình trong playground
Chương trình này in ra kết quả sau:
Area of rectangle 50
Area of circle 452.389342
Thuộc tính trên của các phương thức được sử dụng để triển khai các interface. Chúng ta sẽ thảo luận chi tiết về vấn đề này trong hướng dẫn tiếp theo khi chúng ta xử lý các interface.
Cho đến nay chúng ta chỉ thấy các phương thức với bộ thu giá trị. Có thể tạo các phương thức với bộ thu con trỏ.
Sự khác biệt giữa bộ thu giá trị và bộ thu con trỏ là, người gọi có thể nhìn thấy những thay đổi được thực hiện bên trong một phương thức với bộ thu con trỏ trong khi điều này không đúng với trường hợp của bộ thu giá trị.
Hãy hiểu điều này với sự trợ giúp của một chương trình.
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)
fmt.Printf("\n\nEmployee age before change: %d", e.age)
(&e).changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
Chạy chương trình trong playground
Trong chương trình trên, phương thức changeName
có bộ thu giá trị (e Employee)
trong khi phương thức changeAge
có bộ thu con trỏ (e *Employee)
.
Những thay đổi được thực hiện đối với trường bên trong của struct Employee
sẽ không hiển thị với người gọi và do đó chương trình in cùng một tên trước và sau khi phương thức namechangeNamee.changeName("Michael Andrew")
được gọi ở dòng số 32.
Vì phương thức changeAge(e *Employee)age(&e).changeAge(51)
có bộ thu con trỏ, các thay đổi được áp dụng đối với trường sau khi gọi phương thức sẽ hiển thị cho người gọi. Chương trình này in ra:
Employee name before change: Mark Andrew
Employee name after change: Mark Andrew
Employee age before change: 50
Employee age after change: 51
Ở dòng 36 của chương trình trên, chúng ta sử dụng (&e).changeAge(51)
để gọi phương thức changeAge
. Khi changeAge
có bộ thu con trỏ, chúng ta đã sử dụng (&e)
để gọi phương thức.
Điều này là không cần thiết và ngôn ngữ cung cấp cho chúng ta tùy chọn đơn giản chỉ sử dụng e.changeAge(51)
. e.changeAge(51)
sẽ được thay thế cho (&e).changeAge(51)
.
Chương trình sau được viết lại để sử dụng e.changeAge(51)
thay vì (&e).changeAge(51)
và nó in ra cùng một đầu ra.
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)
fmt.Printf("\n\nEmployee age before change: %d", e.age)
e.changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
Chạy chương trình trong playground
Nói chung, bộ thu con trỏ có thể được sử dụng khi người gọi có thể nhìn thấy các thay đổi được áp dụng đối với bộ thu bên trong phương thức.
Bộ thu con trỏ cũng có thể được sử dụng ở những nơi tốn kém để sao chép cấu trúc dữ liệu. Hãy xem xét một struct có nhiều trường.
Việc sử dụng struct này làm bộ thu giá trị trong một phương thức sẽ cần sao chép toàn bộ struct, điều này sẽ tốn kém.
Trong trường hợp này, nếu sử dụng bộ thu con trỏ, struct sẽ không được sao chép và chỉ một con trỏ tới nó sẽ được sử dụng trong phương thức.
Trong tất cả các tình huống khác, bộ thu giá trị có thể được sử dụng.
Các phương thức thuộc các trường ẩn danh của một struct có thể được gọi như thể chúng thuộc về struct mà trường ẩn danh được định nghĩa.
Hãy xem ví dụ sau:
package main
import (
"fmt"
)
type address struct {
city string
state string
}
func (a address) fullAddress() {
fmt.Printf("Full address: %s, %s", a.city, a.state)
}
type person struct {
firstName string
lastName string
address
}
func main() {
p := person{
firstName: "Elon",
lastName: "Musk",
address: address {
city: "Los Angeles",
state: "California",
},
}
p.fullAddress() //accessing fullAddress method of address struct
}
Chạy chương trình trong playground
Ở dòng 32 của chương trình trên, chúng ta gọi phương thức fullAddress()
của struct address
sử dụng p.fullAddress()
. Cú pháp đầy đủ p.address.fullAddress()
là không cần thiết. Chương trình này in kết quả:
Full address: Los Angeles, California
Chủ đề này hầu hết dành cho người mới. Tôi sẽ cố gắng làm cho nó rõ ràng nhất có thể 😀.
Khi một hàm có một đối số giá trị, nó sẽ chỉ chấp nhận một đối số giá trị.
Khi một phương thức có bộ thu giá trị, nó sẽ chấp nhận cả bộ thu giá trị và bộ thu con trỏ.
Hãy hiểu điều này bằng một ví dụ.
package main
import (
"fmt"
)
type rectangle struct {
length int
width int
}
func area(r rectangle) {
fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}
func (r rectangle) area() {
fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
area(r)
r.area()
p := &r
/*
compilation error, cannot use p (type *rectangle) as type rectangle
in argument to area
*/
//area(p)
p.area()//calling value receiver with a pointer
}
Chạy chương trình trong playground
Hàm func area(r rectangle)
ở dòng số 12 chấp nhận một đối số giá trị và phương thức func (r rectangle) area()
ở dòng số 16 chấp nhận một bộ thu giá trị.
Ở dòng 25, chúng ta gọi hàm area(r)
với một đối số giá trị và nó sẽ hoạt động. Tương tự, chúng ta gọi phương thức r.area()
bằng cách sử dụng bộ thu giá trị và điều này cũng sẽ hoạt động.
Chúng ta tạo một con trỏ p
đến r
dòng số 28. Nếu chúng ta cố gắng truyền con trỏ này đến hàm area(r)
chỉ chấp nhận một giá trị, trình biên dịch sẽ báo lỗi.
Tôi đã comment dòng 33 lại để tránh này. Nếu bạn bỏ comment dòng này, thì trình biên dịch sẽ ném ra lỗi:
compilation error, cannot use p (type *rectangle) as type rectangle in argument to area
Bây giờ đến phần phức tạp, dòng số 35 của mã là p.area()
gọi phương thức area
chỉ chấp nhận một bộ thu giá trị bằng cách sử dụng bộ thu con trỏ p
.
Điều này hoàn toàn hợp lệ. Lý do là dòng p.area()
, để thuận tiện sẽ được Go giải thích là (*p).area()
vì area
có một bộ nhận giá trị.
Chương trình này sẽ xuất ra:
Area Function result: 50
Area Method result: 50
Area Method result: 50
Tương tự như đối số giá trị, các hàm có đối số con trỏ sẽ chỉ chấp nhận con trỏ trong khi các phương thức có bộ thu con trỏ sẽ chấp nhận cả bộ thu con trỏ và bộ thu giá trị.
package main
import (
"fmt"
)
type rectangle struct {
length int
width int
}
func perimeter(r *rectangle) {
fmt.Println("perimeter function output:", 2*(r.length+r.width))
}
func (r *rectangle) perimeter() {
fmt.Println("perimeter method output:", 2*(r.length+r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
p := &r //pointer to r
perimeter(p)
p.perimeter()
/*
cannot use r (type rectangle) as type *rectangle in argument to perimeter
*/
//perimeter(r)
r.perimeter()//calling pointer receiver with a value
}
Chạy chương trình trong playground
Dòng số 12 của chương trình trên định nghĩa một hàm perimeter
chấp nhận một đối số con trỏ và dòng số 17 định nghĩa một phương thức có bộ thu con trỏ.
Dòng 27 chúng ta gọi hàm chu vi với đối số là con trỏ và ở dòng 28, chúng ta gọi phương thức chu vi trên bộ thu con trỏ. Mọi thứ đều tốt.
Trong dòng số 33 được comment, chúng ta cố gắng gọi hàm chu vi với một đối số giá trị r
. Điều này không được phép vì một hàm có đối số con trỏ sẽ không chấp nhận đối số giá trị.
Nếu dòng này được bỏ comment và chương trình đang chạy, quá trình biên dịch sẽ không thành công với lỗi:
main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.
Ở dòng số 35, chúng ta gọi phương thức bộ thu con trỏ perimeter
với bộ thu giá trị r
. Điều này được cho phép và dòng mã r.perimeter()
sẽ được ngôn ngữ chuyển thành (&r).perimeter()
. Chương trình này sẽ xuất ra:
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30
Cho đến nay chúng ta chỉ định nghĩa các phương thức trên các kiểu struct. Cũng có thể định nghĩa các phương thức trên các kiểu không có cấu trúc (non-struct).
Để định nghĩa một phương thức trên một kiểu, định nghĩa của kiểu bộ thu và định nghĩa của phương thức phải có trong cùng một gói.
Cho đến nay, tất cả các struct và các phương thức trên struct mà chúng ta đã định nghĩa đều nằm trong cùng một gói main
và do đó chúng hoạt động.
package main
func (a int) add(b int) {
}
func main() {
}
Chạy chương trình trong playground
Trong chương trình trên, ở dòng số 3, chúng ta đang cố gắng thêm một phương thức có tên add
với kiểu int
. Điều này không được phép vì định nghĩa của phương thức add
và định nghĩa của kiểu int
không nằm trong cùng một gói. Chương trình này sẽ ném ra lỗi biên dịch:
cannot define new methods on non-local type int
Cách để làm cho điều này hoạt động là tạo một bí danh kiểu cho kiểu int
và sau đó tạo một phương thức với bí danh kiểu này làm bộ thu.
package main
import "fmt"
type myInt int
func (a myInt) add(b myInt) myInt {
return a + b
}
func main() {
num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)
fmt.Println("Sum is", sum)
}
Chạy chương trình trong playground
Ở dòng số 5 của chương trình trên, chúng ta đã tạo một kiểu bí danh myInt
cho kiểu int
. Ở dòng số 7, chúng ta đã định nghĩa một phương thức add
với kiểu bộ thu là myInt
.
Chương trình này sẽ in kết quả:
Sum is 15
Như thường lệ, cảm ơn bạn đã đọc. Nếu thấy bài viết này hay, hãy đừng ngần ngại chia sẻ nó cho bạn bè của bạn.
Bạn có thể vui lòng tắt trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi duy trì hoạt động của trang web.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu struct là gì, cách khai báo và sử dụng một struct trong Go, struct ẩn danh, so sanh hai struct, ...
Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách con trỏ (pointer) hoạt động trong Go và nó khác với con trỏ trong các ngôn ngữ khác như C và C++ như thế nào.
Chuỗi (string) xứng đáng được đề cập đặc biệt trong Go vì chúng khác biệt trong cách triển khai khi so sánh với các ngôn ngữ khác.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu map là gì, cú pháp khai báo map, thêm phần tử, xóa phần tử, duyệt các phần tử, ... của map trong Go.