Phương thức (method) trong Go

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.

Giới thiệu về phương thức

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.

Ví dụ về 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

Phương thức so với Hàm

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.

  • Go không phải là một ngôn ngữ lập trình hướng đối tượng thuần túy và nó không hỗ trợ các lớp. Do đó, sử dụng phương thức trên kiểu là một cách để đạt được hành vi tương tự như các lớp. Các phương thức cho phép một nhóm logic các hành vi liên quan đến một kiểu tương tự như các lớp. Trong chương trình mẫu trên, tất cả các hành vi liên quan đến kiểu 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.
  • Các phương thức có cùng tên có thể được định nghĩa trên các kiểu khác nhau trong khi các hàm có cùng tên không được phép. Giả sử rằng chúng ta có hai struct là SquareCircle. Có thể định nghĩa một phương thức có tên Area trên cả SquareCircle. Đ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.

Bộ thu con trỏ so với Bộ thu giá trị

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

Khi nào sử dụng bộ thu con trỏ và khi nào sử dụng bộ thu giá trị

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.

Phương thức của trường ẩn danh trong struct

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  

Bộ thu giá trị trong phương thức so với đối số giá trị trong hàm

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()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  

Bộ thu con trỏ trong phương thức so với đối số con trỏ trong hàm

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  

Phương thức với bộ thu không có cấu trúc

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.

Go
Bài Viết Liên Quan:
Struct trong Go
Trung Nguyen 28/11/2021
Struct trong Go

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, ...

Con trỏ trong Go
Trung Nguyen 28/11/2021
Con trỏ trong Go

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 trong Go
Trung Nguyen 28/11/2021
Chuỗi trong Go

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.

Map trong Go
Trung Nguyen 28/11/2021
Map trong Go

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.