零基础 go - 49(方法)

0 阅读8分钟

在某些情况下,需要声明(定义)一个方法。比如Person结构体除了一些字段(姓名、年龄等)外,

还会有一些行为(吃饭、睡觉、工作等)。这些行为就可以通过方法来实现。

go 中的方法是作用在指定数据类型上的函数,我们可以通过这个数据类型的实例来调用方法。

一、方法的声明和调用

方式

值接收者方法和指针接收者方法

    1. a:接收者,表示方法作用的对象。
    1. type:接收者类型,表示方法作用的数据类型。可以是结构体类型或者非结构体类型。
    1. methodName:方法名,遵循标识符命名规则。
    1. parameters:方法参数列表,参数类型可以是基本类型、结构体类型、接口类型等。参数列表可以为空。
    1. results:方法返回值列表,返回值类型可以是基本类型、结构体类型、接口类型等。返回值列表可以为空。
    1. methodName 方法和 type 这个类型进行绑定,type 这个类型的变量(比如 a)就可以调用 methodName 方法。其他类型的变量不能调用 methodName 方法(除非它们也实现了该方法),methodName 方法也不能直接单独调用。
    1. a 表示 type这个类型的变量的一个副本,方法内对 a 的修改不会影响到调用方法的变量(除非 a 是一个指针类型)。如果需要在方法内修改调用方法的变量,可以将接收者声明为指针类型。
    1. 方法可以有值接收者和指针接收者两种形式。值接收者适用于方法不需要修改调用方法的变量,或者调用方法的变量是一个小型结构体(比如几百字节以下)。指针接收者适用于方法需要修改调用方法的变量,或者调用方法的变量是一个大型结构体(比如几百字节以上)。如果一个类型既有值接收者方法又有指针接收者方法,那么调用方法时会根据调用方法的变量类型自动选择合适的方法。
    1. 方式1不能改变调用方法的变量的值,方式2可以改变调用方法的变量的值。
    1. a 这个变量名可以随意命名,不一定要叫 a

// 方式1:值接收者方法
func (a type) methodName(parameters) (results) {

    // 方法体1

}

// 方式2:指针接收者方法
func (a *type) methodName(parameters) (results) {

    // 方法体2

}

示例代码


type Person struct {

    Name string

    Age int

}

func (p Person) Eat(food string) {

    fmt.Printf("%s is eating %s\n", p.Name, food)

    p.Name = "Changed Name" // 这种修改不会影响调用方法的变量

}

func (p *Person) Sleep(hours int) {

    fmt.Printf("%s is sleeping for %d hours\n", p.Name, hours)

}

func(p *Person) changeName(newName string) {

    p.Name = newName // 等价于 (*p).Name = newName

    fmt.Printf("Name changed to %s\n", p.Name) // 这种修改会影响调用方法的变量

}

func (p Person) String() string {

    return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)

}

func (p Person) Calc(n int) int {

    return n

}

func main() {

    person := Person{Name: "Alice", Age: 30}

    person.Eat("apple") // 调用值接收者方法

    person.Sleep(8) // 调用指针接收者方法

    person.changeName("Bob") // 等价于 (&person).changeName("Bob")

    fmt.Println(person.Name) // 输出: Bob

    fmt.Println(person.String()) // 输出: Person{Name: Bob, Age: 30}

    fmt.Println(person.Calc(5)) // 输出: 5




    var a int

    // a.Eat("apple") // 错误:int类型没有Eat方法

    // Sleep(8) // 错误:无法直接调用方法,必须通过类型实例调用

}

二、方法的调用和传参机制

  • 方法的调用和传参机制与函数类似,但需要注意接收者的类型和值传递/引用传递的区别。

  • 方法调用时,接收者变量会被隐式传递给方法。对于值接收者方法,调用方法时会创建接收者变量的副本;对于指针接收者方法,调用方法时会传递接收者变量的地址。

image.png

image.png

三、方法的注意事项和使用细节

image.png

  • 结构类型是值类型,在方法调用中,遵守值类型的传递机制,方法内对接收者的修改不会影响调用方法的变量(除非接收者是指针类型)。如果需要在方法内修改调用方法的变量,可以将接收者声明为指针类型。

image.png

  • 方法可以访问接收者的字段和其他方法,可以通过接收者调用其他方法(包括值接收者方法和指针接收者方法)。如果一个类型既有值接收者方法又有指针接收者方法,那么调用方法时会根据调用方法的变量类型自动选择合适的方法。

  • 方法可以有返回值,也可以没有返回值。如果方法有返回值,可以在方法体内使用 return 语句返回结果;如果方法没有返回值,可以省略 return 语句,或者使用 return 语句直接结束方法的执行。

  • 方法可以被其他类型实现(即接口),也可以被同一类型的不同实例调用。方法的调用方式和函数类似,可以使用点操作符调用方法,也可以使用函数调用的方式调用方法(即把方法当作函数来调用,显式传递接收者变量)。如果方法被接口实现,那么接口类型的变量也可以调用该方法。

  • 方法的命名规则和函数类似,遵循标识符命名规则,通常使用驼峰命名法。方法名应该能够清晰地表达方法的功能和作用,避免使用过于简短或模糊的名称。方法的参数和返回值也应该有明确的类型和意义,并且在方法体内进行合理的处理和返回。

  • go 中的方法可以绑定所有类型(不仅仅是结构体类型),包括基本类型、数组类型、切片类型、映射类型、接口类型等。只要在方法声明时指定了接收者类型,就可以为该类型定义方法。通过这种方式,可以为现有的类型添加新的行为,而不需要修改原有的类型定义。


package main

import "fmt"

type MyInt int

func (m MyInt) IsEven() bool {

    return m%2 == 0

}

func (m MyInt) noChangeValue() {

    m = 20 // 这种修改不会影响调用方法的变量

}

func (m *MyInt) changeValue() {

    *m = 20 // 这种修改会影响调用方法的变量

}

func main() {

    var num MyInt = 10

    fmt.Printf("%d is even? %v\n", num, num.IsEven()) // 输出: 10 is even? true

    num.noChangeValue()

    fmt.Printf("After noChangeValue: %d\n", num) // 输出: After noChangeValue: 10

    num.changeValue()

    fmt.Printf("After changeValue: %d\n", num) // 输出: After changeValue: 20

}

  • 方法如果是大写字母开头的,那么它就是导出的方法,可以被其他包访问;如果是小写字母开头的,那么它就是未导出的方法,只能在当前包内访问。这与函数的导出规则相同。

  • 如果一个类型实现了 String() 方法,那么 fmt.Println()默认就会调用这个变量的 String() 方法进行输出,而不是输出变量的默认格式。指针类型也会调用 String() 方法,这是因为 fmt 包会检查变量是否实现了 String() 方法,如果实现了就调用它来获取字符串表示。这也是 go 中一种常见的接口实现方式,可以让自定义类型更好地与 fmt 包等标准库配合使用。


package main

import "fmt"

type Person struct {

    Name string

    Age int

}

func (p Person) String() string {

    return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)

}

func main() {

    person := Person{Name: "Alice", Age: 30}

    fmt.Println(person) // 输出: Person{Name: Alice, Age: 30}

  


    p1:= Person{Name: "Bob", Age: 25}

    fmt.Println(&p1) // 输出: Person{Name: Bob, Age: 25},指针类型也会调用 String() 方法

}

四、方法和函数的区别

  • 函数的调用方式为 函数名(实参列表),方法的调用方式为 变量.方法名(实参列表)。

  • 方法调用时会隐式传递接收者变量,而函数调用则需要显式传递所有参数。

  • 方法可以访问接收者的字段和其他方法,而函数只能访问其参数和全局变量。

  • 方法可以被接口实现,而函数不能。方法的定义需要指定接收者类型。

  • 对于普通函数,接受者为值类型时,不能将指针类型的变量作为接收者调用;接受者为指针类型时,可以将值类型的变量作为接收者调用,编译器会自动取地址。


package main

import "fmt"

func ValueMethod(a int) {

    fmt.Printf("%d\n", a)

}

func PointerMethod(a *int) {

    fmt.Printf("%d\n", *a)

}

func main() {

    var x int = 10

    ValueMethod(x) // 正确,调用值接收者方法

    // ValueMethod(&x) // 错误,不能将指针类型的变量作为值接收者调用

    PointerMethod(&x) // 正确,调用指针接收者方法

    // PointerMethod(x) //错误,不能将值类型的变量作为指针接收者调用

}

  • 对于方法,如果调用方法的变量类型与接收者类型不匹配,编译器会尝试进行自动转换(如值类型与指针类型之间),但如果无法转换则会报错。

  • func (a MyInt1) ValueMethod1() {} 这种方式, 内部对 a 的修改不会影响调用方法的变量;

  • func (a *MyInt1) PointerMethod1() {} 这种方式,内部对 a 的修改会影响调用方法的变量。

  • 所以决定是值拷贝还是地址拷贝,要看这个方法的接收者是值类型还是指针类型,而不是看调用方法的变量是值类型还是指针类型。编译器会根据调用方法的变量类型和方法的接收者类型自动选择合适的方法进行调用。


package main

import "fmt"

type MyInt1 int

func (a MyInt1) ValueMethod1() {

    fmt.Printf("%d\n", a)

}

func (a *MyInt1) PointerMethod1() {

    fmt.Printf("%d\n", *a)

}

func main() {

    var x MyInt1 = 10

    x.ValueMethod1() // 正确,调用值接收者方法

    (&x).ValueMethod1() // 错误,不能将指针类型的变量作为值接收者调用

    (&x).PointerMethod1() // 正确,调用指针接收者方法

    x.PointerMethod1() //错误,不能将值类型的变量作为指针接收者调用

}