go☞008方法

阅读量: zyh 2020-10-07 18:00:11
Categories: > Tags:

前言

一个函数是没有接收方的。但当一个函数有了接收方后,则称之为方法

在python中,一个类(接收方)定义方法,直接在类里写方法即可,只不过有一个self指向了它本身。这样,一个类对象就通过方法实现了行为。

在go中,没有类概念,但当自定义的结构作为方法接收方后,结构就既有了属性,又有了行为,从而实现了面向对象编程中的部分功能。

✨并非只有自定义结构可以声明方法,其它类型也可以。

定义

func (<变量> <接收方类型>) 方法名(形参) 方法返回类型{
    ...
}

上面可以看出,方法和函数定义的区别,就是函数名前面添加了一个(变量 接收方类型)变量。

这里的变量仅用于方法内接收方调用自身属性所用。

💥接收方类型必须先创建,即结构必须先创建。

💥方法的定义不能跨包,也就是说必须和接收方在一个包内。

✨方法可以绑定到多个结构类型上。

例子

package main

import "fmt"

// 定义所属者人类
type person struct {
    name string
}

// 定义人类的方法 change 
func (self person) change(){
    self.name = "凡尔赛."+self.name
    fmt.Printf("你的新名字可能变成了:%s\n", self.name)
}

// 主函数
func main(){
    // 声明人类变量 p
    p := person{name:"张三"}
    // 调用 change 方法
    p.change()
    fmt.Printf("你的新名字变更失败,它依然是:%s\n", p.name)
}

上面的例子,有一个现象,就是change()并不能变更person.name

因为方法的接收方person类型,所以p.change()其实是将p复制了一份传递给了方法的self变量。因p和self的内存地址不一样,所以修改self.name不会改变p.name

如何改变?

  1. 将方法内修改后的值通过 return 直接返回
package main

import "fmt"

// 定义所属者人类
type person struct {
    name string
}

// 定义人类的方法 change 
func (self person) change() string{
    self.name = "凡尔赛."+self.name
    fmt.Printf("你的新名字可能变成了:%s\n", self.name)
    return self.name
}

// 主函数
func main(){
    // 声明人类变量 p
    p := person{name:"张三"}
    // 调用 change 方法
    p.name = p.change()
    fmt.Printf("你的新名字是:%s\n", p.name)
}

指针接收方

将方法的接收者变更为指针接收方。

func (self *person) change(){
    self.name = "凡尔赛."+self.name
    fmt.Printf("你的新名字变成了:%s\n", self.name)
}

func main(){
    p := person{name:"张三"}
    (&p).change()
    fmt.Printf("你的新名字是:%s\n", p.name)
}

什么时候会用到方法中的指针?

  1. 参数太大,避免复制

💥根据go的约定,要么结构的所有方法接收方都是指针类型,要么都是值类型,不要混用。

嵌套方法

结构可以嵌套另一个结构,外层结构可以直接调用嵌套结构的方法

package main

import "fmt"

type triangle struct {
    size int
}

type coloredTriangle struct {
    triangle
    color string
}

func (t *triangle) perimeter() int {
    return t.size * 3
}

func (t *triangle) doubleSize() {
    t.size *= 2
}

func main() {
    t := coloredTriangle{
        triangle{3},
        "blue",
    }
    t.doubleSize()
    fmt.Println("NewSize:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}
OldSize: 3
NewSize: 6
Perimeter: 18

coloredTriangle 之所以可以调用 triangle 的方法 doubleSize 和 perimeter,原因在于 go 编译器自动创建了两个包装方法(包装器)。

func (t *coloredTriangle) doubleSize() {
    t.triangle.size = t.triangle.doubleSize()
}

func (t *coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

重载方法

重载方法:表现上就是相同的方法名,不同的接收方。结构对象通过接收方来区分调用的方法。

通过重载方法,可以在不修改主代码的情况下,改变主代码的输出。

以上面的例子来说,就是主动去写包装方法,覆盖掉嵌套时候自动创建的包装方法。

package main

import "fmt"

type triangle struct {
    size int
}

type coloredTriangle struct {
    triangle
    color string
}

func (t *triangle) perimeter() int {
    return t.size * 3
}


// 重载方法perimeter
func (t *coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

func main() {
    t := coloredTriangle{
        triangle{3},
        "blue",
    }
    fmt.Println("Size:", t.size)
    fmt.Println("Color Perimeter:", t.perimeter()) // 调用外结构的重载方法。
    fmt.Println("NoColor Perimeter:", t.triangle.perimeter()) // 显式的调用嵌入结构的方法。
}
Size: 3
Color Perimeter: 18
NoColor Perimeter: 9

封装方法

封装可以让方法或者属性变成私有,即仅可用于包内部。通过包的封装方法,可以仅提供部分方法供调用包的程序所用。

go的封装方法仅用于程序包之间。

将方法或者属性名的首字母大写即为公开,小写即为私有。

➜   tree
.
├── geometry
│   ├── geometry.go
│   └── go.mod # go mod init geometry
├── go.mod # go mod init 022
└── main.go

➜   cat go.mod
module 022

go 1.17

require geometry v0.0.0
replace geometry => ./geometry
//geometry.go
package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}
//main.go
package main

import (
    "fmt"
    "geometry"
)

func main(){
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
    //fmt.Print(t.size)
}
➜   go run main.go
Perimeter 18

如果调用geometry私有属性size,则会报错

# command-line-arguments
./main.go:12:18: t.size undefined (cannot refer to unexported field or method size)

其它类型的方法

默认情况下,无法基于基础类型声明方法,例如无法直接给string类型声明方法。

但是可以基于基础类型创建新的类型,然后针对新的类型声明方法

package main

import (
    "fmt"
    "strings"
)

type stringupper string

func(s stringupper) Upper() string {
    return strings.ToUpper(string(s)) // s 是 stringupper 类型,因此需要转成 string 类型,才可以被 ToUpper 方法调用
}

func main(){
    s := stringupper("this is string") // stringupper 类型拥有 string() 方法,因此直接()传递字符串.
    fmt.Println(s)
    fmt.Println(s.Upper())
}
➜   go run 023.go
this is string
THIS IS STRING