Go教程第十二篇: 方法

2020-03-27  本文已影响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中的方法,祝您愉快!

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

上一篇下一篇

猜你喜欢

热点阅读