美文网首页
Go教程第十二篇: 方法

Go教程第十二篇: 方法

作者: 大风过岗 | 来源:发表于2020-03-27 11:11 被阅读0次

    方法

    本文我们讲述方法。

    简介

    在func关键字和方法名之间存在接收者类型的那些函数,我们称之为方法。接收者可以是结构体类型,也可以是非结构体类型。

    方法声明的语法,如下:

    func (t Type) methodName(parameter list) {
    
    }
    
    

    上面这个代码片,就是声明了一个名为methodName的方法,它的接收者类型是Type。t被称为接收者,在方法内部可以访问接收者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
    }
    
    

    在上面的程序中,我们在Employee结构体上创建了一个displaySalary方法。displaySalary()方法访问了接收者e,同时使用接收者e打印出了name、currency、salary。我们使用了语法 emp1.displaySalary()语法调用了此方法。程序的输出为:Salary of Sam Adolf is $5000。

    方法 VS 函数

    上面的程序,也可以用函数重写,完全可以不用方法。

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

    上面的程序,把displaySalary方法改成了一个函数,这个函数接收了一个Employee结构体作为参数,同时这个函数也和displaySalary产生了相同的输出。Salary of Sam Adolf is $5000。

    那么,既然我们可以使用函数来完成和方法完全相同的工作,我们为什么还要方法呢?这样做有很多理由,我们来一个个看看这些理由是什么。

    . Go并不是一个纯粹的面向对象的语言,它不支持类。因此,方法是实现和类相似的一种方式。我们可以为类型创建一组方法。
    在上面的程序中,和Employee类相关的所有行为都可以用Employee做为接收者类型来创建方法。例如,我们可以创建方法: calculatePension、calculateLeaves等等。

    . 具有相同名称的方法可以定义在不同的类型上。我们假定有俩个结构体:Square和Circle。那么我们可以在Square和Circle上都创建一个名为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())
    }
    
    

    程序的输出如下:

    Area of rectangle 50
    Area of circle 452.389342
    
    

    上面这些方法的特性可用于实现接口。我们将在下一篇教程中详细讨论接口。

    指针接收者 VS 值接收者

    到目前为止,我们已经了解了拥有值接收者的方法,除此之外,我们还可以创建带有指针接收者的方法。值接收者和指针接收者之间的不同之处在于:对带有指针接收者的方法做出的修改对调用者来说是可见的。而值接收者却并非这样。我们用下面这个程序来理解一下。

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

    在上面的程序中,changeName方法有一个值接收者(e Employee)而changeAge方法有一个指针接收者(e *Employee)。在changeName中对Employee结构体的name字段做出的修改对其调用者来说是不可见的。因此,在方法e.changeName("Michael Andrew")调用的前后打印的name值都是相同的。
    而changeAge方法有一个指针接收者(e *Employee),在函数调用(&e).changAge(51)之后,对age字段的修改对其调用者来说是可见的。
    程序的输出如下:

    Employee name before change: Mark Andrew
    Employee name after change: Mark Andrew
    
    Employee age before change: 50
    Employee age after change: 51
    
    

    在上面的程序中,我们使用(&e).changeAge(51)来调用changeAge()方法。由于changeAge有一个指针接收者,我们使用(&e)来调用此方法,但是不必要这样调用,Go为我们提供了另一个选择,我们可以使用e.changAge(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)
    }
    
    

    什么时候使用指针接收,什么时候使用值接收

    通常来说,当对接收者做出的修改需要对调用者可见时,就使用指针接收者。此外,在某些复制数据结构体代价比较大时,也可以使用指针接收者。
    设想一下,如果某个结构体有许多字段,那么使用此结构体作为值接收者的话,就需要复制全部的结构体,而这样复制的操作是非常昂贵的。
    在这种情况下,如果使用指针接收者的话,就不需要复制结构体,只需要一个指针即可。在其他场景中,可以使用值接收者。

    匿名结构体字段的方法

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

    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
    
    }
    
    

    在上面的程序中,我们使用p.fullAddress()调用了address结构体的fullAddress方法。没必要使用p.address.fullAddress()来调用,它们的输出是一样的。

    Full address: Los Angeles, California
    
    

    方法的值接收者 VS 函数的值参数

    这个话题主要是针对初学者,因此,我会尽可能讲解的更清楚。

    若函数只有一个值参的话,那么它只能接收一个值参。但若方法有一个值接收者的话,它可以同时接收指针和值接收者。

    我们用一个案例来理解这一点。

    
    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
    }
    
    

    函数func area(r rectangle)接收一个值参,同时方法func (r rectangle) area()接收了一个值接收者。当我们用area(r)调用这个函数时,它可以正常运行,相似地,我们使用值接收者调用方法r.area()时,它也可以正常运行。

    在程序中,我们还创建了一个指向r的指针。如果把这个指针传递给函数area的话,编译器会报错compilation error, cannot use p (type *rectangle) as type rectangle in argument to area。

    现在我们来看最棘手的部分,程序中代码p.area()使用指针接收者p调用了方法area,而方法area是只接受值接收者的。但是这样却没问题,原因是p.area()会被Go解释为(*p).area()

    程序将输出如下:

    
    Area Function result: 50
    Area Method result: 50
    Area Method result: 50
    
    

    方法的指针接收者 VS 函数的指针参数

    和值参数类似,指针参数的函数只能接收指针。然而 指针接收者的方法却可以同时接收指针和值接收者。

    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
    
    }
    
    

    在上面的程序中,定义了一个函数perimeter,它接收指针参数。定义了一个方法perimeter,它接收指针接收者。
    当我们使用传递值参给函数perimeter时,编译器是不允许的。因为指针参数的函数不能接收值参。
    而当我们用一个值接收者r调用方法perimeter的时候,却没问题。代码r.perimeter()会被编译器解释为(&r).perimeter()。
    程序的输出如下:

    perimeter function output: 30
    perimeter method output: 30
    perimeter method output: 30
    
    

    非结构体接收者的函数

    到目前为止,我们都是在结构体类型上定义方法。但其实,在非结构体类型上,也能定义方法。但是有个限制是,在类型上定义方法时,接收者的定义和方法定义必须在同一个包下。我们现在都是在main包下面定义的,因此,他们都没问题。

    package main
    
    func (a int) add(b int) {
    }
    
    func main() {
    
    }
    
    

    在上面的程序中,我们试图在内置的数据类型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)
    }
    
    

    在上面的程序中,我们为int创建了一个类型别名myInt,然后我们定义了一个以myInt作为接收者的add方法。
    程序将输出:Sum is 15 。

    上述这些就是Go中的方法,祝您愉快!

    本篇系翻译之作,原文地址

    相关文章

      网友评论

          本文标题:Go教程第十二篇: 方法

          本文链接:https://www.haomeiwen.com/subject/eefluhtx.html