• Go 中的方法


    方法介绍

    方法只是一个func关键字和方法名称之间具有特殊接收者类型的函数,接收者可以是结构类型或非结构类型。

    下面提供了方法声明的语法。

    func (t Type) methodName(parameter list) {  
    }
    
    • 1
    • 2

    上面的代码片段创建了一个methodName以接收者Type类型命名的方法。t被称为接收者并且可以在方法内访问它。

    示例方法

    让我们编写一个简单的程序,在结构类型上创建一个方法并调用它。

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    Run in playground

    上面的程序种,我们在Employee结构类型上创建了一个displaySalary方法。该displaySalary()方法可以访问e其内部的接收器。

    在17行中,我们正在使用接收器e并打印员工的姓名、货币和工资。

    在26 行中。我们已经使用语法调用了该方法emp1.displaySalary()

    该程序打印

    Salary of Sam Adolf is $5000
    
    • 1

    方法与函数

    上面的程序可以只使用函数而不使用方法来重写。

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    Run in playground

    在上面的程序中,displaySalary方法被转换为函数,并且Employee结构体作为参数传递给它。该程序也产生完全相同的输出

    Salary of Sam Adolf is $5000
    
    • 1

    那么,当我们可以使用函数编写相同的程序时,为什么还要有方法呢?造成这种情况的原因有几个。让我们一一看看。

    • Go 不是一种纯粹的面向对象编程语言,它不支持类。因此,类型上的方法是实现类似于类的行为的一种方式。方法允许对与类似于类的类型相关的行为进行逻辑分组。在上面的示例程序中,所有与Employee类型相关的行为都可以通过使用Employee接收者类型创建方法来分组。例如,我们可以添加calculatePensioncalculateLeaves等方法。
    • 可以在不同类型上定义具有相同名称的方法,但不允许在不同类型上定义具有相同名称的函数。假设我们有一个SquareandCircle结构。可以在这个2个结构中定义一个Area命名的方法。这是在下面的程序中完成的。
    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())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    Run in playground

    该程序打印

    Area of rectangle 50  
    Area of circle 452.389342  
    
    • 1
    • 2

    方法的上述属性用于实现接口。我们将在下一个教程中处理接口时详细讨论这一点。

    指针接收器与值接收器

    到目前为止,我们只看到了带有值接收者的方法。也可以创建带有指针接收器的方法。

    值接收器和指针接收器之间的区别在于,使用指针接收器的方法内部所做的更改对于调用者来说是可见的,而在值接收器中则不是这样。

    让我们借助一个程序来理解这一点。

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    Run in playground

    在上面的程序中,该changeName方法有一个(e Employee)值接收器,而该changeAge方法有一个(e *Employee)指针接收器。

    Employee结构体name内部字段所做的更改changeName对调用者来说是不可见的,因此程序调用该方法之前和之后打印相同的名称。

    由于changeAge方法有一个(e *Employee)指针接收者,因此age方法调用后对字段所做的更改(&e).changeAge(51)将对调用者可见。该程序打印,

    Employee name before change: Mark Andrew  
    Employee name after change: Mark Andrew
    
    Employee age before change: 50  
    Employee age after change: 51  
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面的程序中,我们使用(&e).changeAge(51)来调用该changeAge方法。既然changeAge有指针接收器,我们就用来(&e)调用该方法。这是不需要的,我们还可以选择只使用e.changeAge(51)e.changeAge(51)将被解释为由(&e).changeAge(51)

    以下程序被重写以使用 e.changeAge(51)代替(&e).changeAge(51)并打印相同的输出。

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    Run in playground

    何时使用指针接收器以及何时使用值接收器

    通常,当方法内部对接收器所做的更改应该对调用者可见时,可以使用指针接收器。

    指针接收器也可以用在复制数据结构成本高昂的地方。

    考虑一个具有许多字段的结构。在方法中使用此结构作为值接收器将需要复制整个结构,这将是昂贵的。在这种情况下,如果使用指针接收器,则不会复制该结构,并且在方法中仅使用指向它的指针。

    在所有其他情况下,都可以使用值接收器。

    匿名结构体字段的方法

    属于结构体匿名字段的方法可以被调用,就好像它们属于定义匿名字段的结构体一样。

    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
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    Run in playground

    在上面程序的第32行中,我们使用调用结构体fullAddress()的方法。不需要明确的指示。该程序打印

    Full address: Los Angeles, California  
    
    • 1

    方法中的值接收器与函数中的值参数

    这个话题最适合 Go 新手。我会尽力让它尽可能清楚。

    当函数有值参数时,它将只接受值参数。

    当一个方法有一个值接收器时,它将同时接受指针和值接收器。

    让我们通过一个例子来理解这一点。

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    Run in playground

    func area(r rectangle)函数接受一个值参数和func (r rectangle) area()的方法接受一个值参数。

    在25行中我们用一个值参数调用area函数area(r),它就会工作。类似地,我们使用值接收器调用区域方法r.area(),这也可以工作。

    我们创建一个p指向r指针。

    如果我们试图将这个指针传递给只接受一个值的函数区域,编译器会报错。

    如果不注释此行,则编译器将抛出错误*编译错误,cannot use p (variable of type rectangle) as rectangle value in argument to area

    调用使用指针接收器仅接受值接收器的p.area()方法。这是完全有效的。原因是(*p).area() 将被 Go 解释为一个值接收器。

    该程序将输出,

    Area Function result: 50  
    Area Method result: 50  
    Area Method result: 50  
    
    • 1
    • 2
    • 3

    方法中的指针接收器与函数中的指针参数

    与值参数类似,具有指针参数的函数仅接受指针,而具有指针接收器的方法将同时接受指针和值接收器。

    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
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    Run in playground

    上述程序的第 12 行定义了一个接受指针参数的函数。17行 定义了一个具有指针接收器的方法。

    在第 27 行中,我们使用指针参数调用 perimeter 函数,在第 28 行中,我们在指针接收器上调用 perimeter 方法。一切都很好。

    在第 33 行注释中,我们尝试使用值参数调用函数perimeter(r)。这是不允许的,因为带有指针参数的函数不接受值参数。如果取消注释此行并且运行程序,则编译将失败,并显示错误main.go:33: Cannot use r (typefragment) as type *rectangle in argument to perimeter

    在第 35 行中,我们使用 r.perimeter()值接收器调用指针接收器方法。这是允许的,并且为了方便起见,该语言将解释该代码行为(&r).perimeter()

    该程序将输出,

    perimeter function output: 30  
    perimeter method output: 30  
    perimeter method output: 30  
    
    • 1
    • 2
    • 3

    具有非结构接收器的方法

    到目前为止,我们仅在结构类型上定义了方法。也可以在非结构类型上定义方法.

    但有一个问题。要在类型上定义方法,接收者类型的定义和方法的定义应存在于同一包中。

    到目前为止,我们定义的所有结构体和结构体上的方法都位于同一个main包中,因此它们可以工作。

    package main
    
    func (a int) add(b int) {  
    }
    
    func main() {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Run in playground

    在上面的程序中,我们正在尝试添加一个名为int内置类型的add方法。这是不允许的,因为方法的定义add和类型的定义int不在同一个包中。该程序将抛出编译错误cannot define new methods on non-local type int

    实现此功能的方法是为内置类型创建类型别名int,然后使用该类型别名创建一个方法作为接收者。

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Run in playground

    在上述程序的第 5 行中,我们创建了一个int类型别名myInt。在7行中,我们定义了一个方法add作为myInt接收者。

    该程序将打印Sum is 15.

    这就是 Go 中的方法。祝你有美好的一天。

  • 相关阅读:
    es6新增-Promise详解(异步编程的解决方案1)
    【附源码】计算机毕业设计JAVA疫情下的居民管理系统
    看了那么多文章,终于弄懂了脏读、不可重复度、幻读
    CVE-2021-26084 漏洞分析
    【Hack The Box】windows练习-- Omni
    [翻译].NET 8 的原生AOT及高性能Web开发中的应用[附性能测试结果]
    分布式之日志系统平台ELK
    C#数组简单介绍
    创新洞见|2023年B2B业务为何必须采用PLG增长策略
    Git在已有的项目中引入Submodule子模块管理:添加、更新、删除(实战示例代码)
  • 原文地址:https://blog.csdn.net/qq497811258/article/details/133941239