欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Go基础系列:Go中的方法

程序员文章站 2022-04-28 17:52:10
Go方法简介 Go中的struct结构类似于面向对象中的类。面向对象中,除了成员变量还有方法。 Go中也有方法,它是一种特殊的函数,定义于struct之上(与struct关联、绑定),被称为struct的receiver。 它的定义方式大致如下: 这表示 函数是绑定在mytype这个struct t ......

go方法简介

go中的struct结构类似于面向对象中的类。面向对象中,除了成员变量还有方法。

go中也有方法,它是一种特殊的函数,定义于struct之上(与struct关联、绑定),被称为struct的receiver。

它的定义方式大致如下:

type mytype struct{}

func (recv mytype) my_method(para) return_type {}
func (recv *mytype) my_method(para) return_type {}

这表示my_method()函数是绑定在mytype这个struct type上的,是与之关联的,是独属于mytype的。所以,此函数称为"方法"。所以,方法和字段一样,也是struct类型的一种属性。

其中方法名前面的(recv mytype)(recv *mytype)是方法的receiver,具有了receiver的函数才能称之为方法,它将函数和type进行了关联,使得函数绑定到type上。至于receiver的类型是mytype还是*mytype,后面详细解释。

定义了属于mytype的方法之后,就可以直接通过mytype来调用这个方法:

mytype.my_method()

来个实际的例子,定义一个名为changfangxing的struct类型,属性为长和宽,定义属于changfangxing的求面积的方法area()。

package main

import "fmt"

type changfangxing struct {
    length float64
    width  float64
}

func (c *changfangxing) area() float64 {
    return c.length * c.width
}

func main() {
    c := &changfangxing{
        2.5,
        4.0,
    }
    fmt.printf("%f\n",c.area())
}

方法的一些注意事项

1.方法的receiver type并非一定要是struct类型,type定义的类型别名、slice、map、channel、func类型等都可以。但内置简单数据类型(int、float等)不行,interface类型不行

package main

import "fmt"

type myint int

func (i *myint) numadd(n int) int {
    return n + 1
}

func main() {
    n := new(myint)
    fmt.println(n.numadd(4))
}

以slice为类型,定义属于它的方法:

package main

import "fmt"

type myslice []int

func (v myslice) sumofslice() int {
    sum := 0
    for _, value := range v {
        sum += value
    }
    return sum
}

func main() {
    s := myslice{11, 22, 33}
    fmt.println(s.sumofslice())
}

2.struct结合它的方法就等价于面向对象中的类。只不过struct可以和它的方法分开,并非一定要属于同一个文件,但必须属于同一个包。所以,没有办法直接在int、float等内置的简单类型上定义方法,真要为它们定义方法,可以像上面示例中一样使用type定义这些类型的别名,然后定义别名的方法

3.方法有两种类型(t type)(t *type),它们之间有区别,后文解释。

4.方法就是函数,所以go中没有方法重载(overload)的说法,也就是说同一个类型中的所有方法名必须都唯一。但不同类型中的方法,可以重名。例如:

func (a *mytype1) add() ret_type {}
func (a *mytype2) add() ret_type {}

5.type定义类型的别名时,别名类型不会拥有原始类型的方法。例如mytype上定义了方法add(),mytype的别名new_type不会有这个方法,除非自己重新定义。

6.如果receiver是一个指针类型,则会自动解除引用。例如,下面的a是指针,它会自动解除引用使得能直接调用属于mytype1实例的方法add()。

func (a *mytype1) add() ret_type {}
a.add()

7.(t type)(t *type)的t,其实就是面向对象语言中的this或self,表示调用该实例的方法。如果愿意,自然可以使用self或this,例如(self type),但这是可以随意的。

8.方法和type是分开的,意味着实例的行为(behavior)和数据存储(field)是分开的,但是它们通过receiver建立起关联关系

方法和函数的区别

其实方法本质上就是函数,但方法是关联了类型的,可以直接通过类型的实例去调用属于该实例的方法。

例如,有一个type person,如果定义它的方法setname()和定义通用的函数setname2(),它们要实现相同的为person赋值名称时,参数不一样:

func (p *person) setname(name string) {
    p.name = name
}

func setname2(p *person,name string) {
    p.name = name
}

通过函数为person的name赋值,必须将person的实例作为函数的参数之一,而通过方法则无需声明这个额外的参数,因为方法是关联到person实例的。

值类型和指针类型的receiver

假如有一个person struct:

type person struct{
    name string
    age int
}

有两种类型的实例:

p1 := new(person)
p2 := person{}

p1是指针类型的person实例,p2是值类型的person实例。虽然p1是指针,但它也是实例。在需要访问或调用person实例属性时候,如果发现它是一个指针类型的变量,go会自动将其解除引用,所以p1.name在内部实际上是(*p1).name。同理,调用实例的方法时也一样,有需要的时候会自动解除引用。

除了实例有值类型和指针类型的区别,方法也有值类型的方法和指针类型的区别,也就是以下两种receiver:

func (p person) setname(name string) { p.name = name }
func (p *person) setage(age int) { p.age = age }

setname()方法中是值类型的receiver,setage()方法中是指针类型的receiver。它们是有区别的。

首先,setage()方法的p是一个指针类型的person实例,所以方法体中的p.age实际上等价于(*p).age

再者,方法就是函数,go中所有需要传值的时候,都是按值传递的,也就是拷贝一个副本

setname()中,除了参数name string需要拷贝,receiver部分(p person)也会拷贝,而且它明确了要拷贝的对象是值类型的实例,也就是拷贝完整的person数据结构。但实例有两种类型:值类型和指针类型。(p person)无视它们的类型,因为receiver严格规定p是一个值类型的实例。所以无论是指针类型的p1实例还是值类型的p2实例,都会拷贝整个实例对象。对于指针类型的实例p1,前面说了,在需要的时候,go会自动解除引用,所以p1.setname()等价于(*p1).setname()

也就是说,只要receiver是值类型的,无论是使用值类型的实例还是指针类型的实例,都是拷贝整个底层数据结构的,方法内部访问的和修改的都是实例的副本。所以,如果有修改操作,不会影响外部原始实例。

setage()中,receiver部分(p *person)明确指定了要拷贝的对象是指针类型的实例,无论是指针类型的实例p1还是值类型的p2,都是拷贝指针。所以p2.setage()等价于(&p2).setage()

也就是说,只要receiver是指针类型的,无论是使用值类型的实例还是指针类型的实例,都是拷贝指针,方法内部访问的和修改的都是原始的实例数据结构。所以,如果有修改操作,会影响外部原始实例。

那么选择值类型的receiver还是指针类型的receiver?一般来说选择指针类型的receiver。

下面的代码解释了上面的结论:

package main

import "fmt"

type person struct {
    name string
    age  int
}

func (p person) setname(name string) {
    p.name = name
}
func (p *person) setage(age int) {
    p.age = age
}

func (p *person) getname() string {
    return p.name
}
func (p *person) getage() int {
    return p.age
}

func main() {
    // 指针类型的实例
    p1 := new(person)
    p1.setname("longshuai1")
    p1.setage(21)
    fmt.println(p1.getname()) // 输出""
    fmt.println(p1.getage())  // 输出21

    // 值类型的实例
    p2 := person{}
    p2.setname("longshuai2")
    p2.setage(23)
    fmt.println(p2.getname())  // 输出""
    fmt.println(p2.getage())   // 输出23
}

上面分别创建了指针类型的实例p1和值类型的实例p2,但无论是p1还是p2,它们调用setname()方法设置的name值都没有影响原始实例中的name值,所以getname()都输出空字符串,而它们调用setage()方法设置的age值都影响了原始实例中的age值。

嵌套struct中的方法

当内部struct嵌套进外部struct时,内部struct的方法也会被嵌套,也就是说外部struct拥有了内部struct的方法。

例如:

package main

import (
    "fmt"
)

type person struct{}

func (p *person) speak() {
    fmt.println("speak in person")
}

// admin exported
type admin struct {
    person
    a int
}

func main() {
    a := new(admin)
    // 直接调用内部struct的方法
    a.speak()
    // 间接调用内部stuct的方法
    a.person.speak()
}

当person被嵌套到admin中后,admin就拥有了person中的属性,包括方法speak()。所以,a.speak()a.person.speak()都是可行的。

如果admin也有一个名为speak()的方法,那么admin的speak()方法将掩盖内部struct的person的speak()方法。所以a.speak()调用的将是属于admin的speak(),而a.preson.speak()将调用的是person的speak()。

验证如下:

func (a *admin) speak() {
    fmt.println("speak in admin")
}

func main() {
    a := new(admin)
    // 直接调用内部struct的方法
    a.speak() 
    // 间接调用内部stuct的方法
    a.person.speak()
}

输出结果为:

speak in admin
speak in person

嵌入方法的第二种方式

除了可以通过嵌套的方式获取内部struct的方法,还有一种方式可以获取另一个struct中的方法:将另一个struct作为外部struct的一个命名字段

例如:

type person struct {
    name string
    age int
}
type admin struct {
    people *person
    salary int
}

现在admin除了自己的salary属性,还指向一个person。这和struct嵌套不一样,struct嵌套是直接外部包含内部,而这种组合方式是一个struct指向另一个struct,从admin可以追踪到其指向的person。所以,它更像是链表。

例如,person是admin type中的一个字段,person有方法speak()。

package main

import (
    "fmt"
)

type person struct {
    name string
    age  int
}

type admin struct {
    people *person
    salary int
}

func main() {
    // 构建admin实例
    a := new(admin)
    a.salary = 2300
    a.people = new(person)
    a.people.name = "longshuai"
    a.people.age = 23
    // 或a := &admin{&person{"longshuai",23},2300}

    // 调用属于person的方法speak()
    a.people.speak()
}

func (p *person) speak() {
    fmt.println("speak in person")
}

或者,定义一个属于admin的方法,在此方法中应用person的方法:

func (a *admin) sing(){
    a.people.speak()
}

然后只需调用a.sing()就可以隐藏person的方法。

多重继承

因为go的struct支持嵌套多个其它匿名字段,所以支持"多重继承"。这意味着外部struct可以从多个内部struct中获取属性、方法。

例如,照相手机cameraphone是一个struct,其内嵌套phone和camera两个struct,那么cameraphone就可以获取来自phone的call()方法进行拨号通话,获取来自camera()的takeapic()方法进行拍照。

面向对象的语言都强烈建议不要使用多重继承,甚至有些语言本就不支持多重继承。至于go是否要使用"多重继承",看需求了,没那么多限制。

重写string()方法

fmt包中的println()、print()和printf()的%v都会自动调用string()方法将待输出的内容进行转换。

可以在自己的struct上重写string()方法,使得输出这个示例的时候,就会调用它自己的string()。

例如,定义person的string(),它将person中的name和age结合起来:

package main

import (
    "fmt"
    "strconv"
)

type person struct {
    name string
    age  int
}

func (p *person) string() string {
    return p.name + ": " + strconv.itoa(p.age)
}

func main() {
    p := new(person)
    p.name = "longshuai"
    p.age = 23
    // 输出person的实例p,将调用string()
    fmt.println(p)
}

上面将输出:

longshuai: 23

一定要注意,定义struct的string()方法时,string()方法里不要出现fmt.print()、fmt.println以及fmt.printf()的%v,因为它们自身会调用string(),会出现无限递归的问题。