在这里插入图片描述

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》


在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体 :除了有一些字段外(年龄,姓名…),Persgn结构体还有一些行为比如:可以说话、跑步.通过学习,还可以做算术题。这时就要用方法才能完成。

Golang中的方法是 作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct

1. 核心概述

1.1 方法定义

Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

  • 只能为当前包内命名类型定义方法。
  • 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
  • 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
  • 不支持方法重载,receiver 只是参数签名的组成部分。
  • 可用实例 value 或 pointer 调用全部方法,编译器自动转换。

理解:一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

语法如下

func (recevier type) methodName(参数列表)(返回值列表){
    方法体
    return 返回值
}
  • 参数列表:表示方法输入
  • recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
  • receiver type:type可以是结构体,也可以其它的自定义类型
  • receiver:就是type类型的一个变量(实例),比如:Person结构体的一个变量(实例)
  • 参数列表:表示方法输入返回值列表:表示返回的值,可以多个
  • 方法主体:表示为了实现某一功能代码块
  • return 语句不是必须的

举例说明

// 定义结构体
type Person struct {
    Name string
    Age  int
}

// 为 Person 类型定义方法
func (p Person) SayHello() {
    fmt.Printf("Hello, I'm %s, %d years old\n", p.Name, p.Age)
}

// 带返回值的方法
func (p Person) GetAge() int {
    return p.Age
}

// 指针接收者方法
func (p *Person) SetAge(age int) {
    p.Age = age
}

使用如下

func main() {
    // 创建结构体实例
    person := Person{Name: "张三", Age: 25}
    
    // 调用方法
    person.SayHello()
    fmt.Printf("年龄: %d\n", person.GetAge())
    
    // 调用指针接收者方法
    person.SetAge(30)
    fmt.Printf("修改后年龄: %d\n", person.Age)
}

注意:方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。

1.2 接收者类型
  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如果程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理

值接收者 vs 指针接收者

定义如下的结构体:

type Counter struct {
    Count int
}

func (c Counter) GetValue() int {
    return c.Count
}

代码示例如下

// 值接收者:不会修改原对象
func (c Counter) IncrementByValue() {
    c.Count++  // 只修改副本
    fmt.Printf("值接收者内部 Count: %d\n", c.Count) 
}

// 指针接收者:修改原对象
func (c *Counter) IncrementByPointer() {
    c.Count++  
    fmt.Printf("指针接收者内部 Count: %d\n", c.Count)
}

调用如下

func main() {
    counter := Counter{Count: 0}
    fmt.Printf("初始 Count: %d\n", counter.GetValue())
    
    // 调用值接收者方法
    counter.IncrementByValue()
    fmt.Printf("调用值接收者方法后: %d\n", counter.GetValue())  // 仍然是 0
    
    // 调用指针接收者方法
    counter.IncrementByPointer()
    fmt.Printf("调用指针接收者方法后: %d\n", counter.GetValue())  // 变成 1
    
    counterPtr := &counter // 指针调用
    counterPtr.IncrementByValue()   // 仍然是 1
    counterPtr.IncrementByPointer() // 变成 2
    fmt.Printf("指针调用后: %d\n", counter.GetValue()) // 2
}

解释

  • 首先我们定义了一个叫做 Counter 的结构体类型,然后定义了一个该类型的方法叫做 Increment,该方法的接受者是一个 Counter 类型的值。要调用 Increment 方法我们需要一个 Counter 类型的值或者指针。
  • 在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作。

注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作。

方法不过是一种特殊的函数,只需将其还原,就知道 receiver T 和 *T 的差别。

type Data struct {
    x int
}

func (self Data) ValueTest() { // func ValueTest(self Data);
    fmt.Printf("Value: %p\n", &self)
}

func (self *Data) PointerTest() { // func PointerTest(self *Data);
    fmt.Printf("Pointer: %p\n", self)
}

func main() {
    d := Data{}
    p := &d
    fmt.Printf("Data: %p\n", p)

    d.ValueTest()   // ValueTest(d)
    d.PointerTest() // PointerTest(&d)

    p.ValueTest()   // ValueTest(*p)
    p.PointerTest() // PointerTest(p)
}

// 输出: 
Data: 0xc000010070
Value: 0xc000010080
Pointer: 0xc000010070
Value: 0xc000010088
Pointer: 0xc000010070

一些说明

  1. Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法
  2. 比如:如果一个变量实现了 String() 这个方法,那么fmt.Println默认会调用这个变量的 String() 进行输出

我们还可以为基本类型定义方法,如下:

// 为基本类型定义别名
type MyInt int
type Celsius float64

// 为自定义类型定义方法
func (mi MyInt) Double() MyInt {
    return mi * 2
}

func (mi *MyInt) Square() {
    *mi = *mi * *mi
}

func (c Celsius) String() string {
    return fmt.Sprintf("自己的 printf %.1f°C", c)
}

func main() {
    // MyInt 类型的方法
    var num MyInt = 5
    fmt.Printf("%d 的两倍: %d\n", num, num.Double())
    
    num.Square()
    fmt.Printf("%d 的平方: %d\n", 5, num)
    
    // Celsius 类型的方法
    temp := Celsius(25.0)
    fmt.Println(temp)  // 自动调用 String() 方法
}

// 输出
5 的两倍: 10
5 的平方: 25
自己的 printf 25.0°C
1.3 方法 vs 函数
  1. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
  2. 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。
  3. 调用方式
    • 函数:函数名(实参列表)
    • 方法:变量.方法名(实参列表)

普通函数

//接收值类型参数的函数
func valueIntTest(a int) int {
    return a + 10
}

//接收指针类型参数的函数
func pointerIntTest(a *int) int {
    return *a + 10
}

func structTestValue() {
    a := 2
    fmt.Println("valueIntTest:", valueIntTest(a)) // 12
    //函数的参数为值类型,则不能直接将指针作为参数传递
    //fmt.Println("valueIntTest:", valueIntTest(&a))
    //compile error: cannot use &a (type *int) as type int in function argument

    b := 5
    fmt.Println("pointerIntTest:", pointerIntTest(&b)) // 15
    //同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
    //fmt.Println("pointerIntTest:", pointerIntTest(&b))
    //compile error:cannot use b (type int) as type *int in function argument
}

方法

type PersonD struct {
    id   int
    name string
}

// 接收者为值类型
func (p PersonD) valueShowName() {
    fmt.Println(p.name)
}

// 接收者为指针类型
func (p *PersonD) pointShowName() {
    fmt.Println(p.name)
}

func structTestFunc() {
    //值类型调用方法
    personValue := PersonD{101, "hello world"}
    personValue.valueShowName()
    personValue.pointShowName()

    //指针类型调用方法
    personPointer := &PersonD{102, "hello golang"}
    personPointer.valueShowName()
    personPointer.pointShowName()

    //与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
}

如果两个都使用结构体作为参数的话,那么如下:

结构体定义如下

type Rectangle struct {
    w, h float64
}

函数方式

func area(r Rectangle) float64 {
    return r.Width * r.Height
}

func perimeter(r Rectangle) float64 {
    return 2 * (r.Width + r.Height)
}

方法方式

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

调用如下

func main() {
    rect := Rectangle{Width: 5.0, Height: 3.0}
    
    // 函数调用
    fmt.Printf("函数计算面积: %.2f\n", area(rect))
    fmt.Printf("函数计算周长: %.2f\n", perimeter(rect))
    
    // 方法调用
    fmt.Printf("方法计算面积: %.2f\n", rect.Area())
    fmt.Printf("方法计算周长: %.2f\n", rect.Perimeter())
    
    // 方法链式调用的可能性
    fmt.Printf("面积: %.2f, 周长: %.2f\n", rect.Area(), rect.Perimeter())
}

2. 语法糖优化

在 Go 语言中,当你使用指针接收者时,Go 会自动为你做语法糖优化,提升可读性

示例

type Circle struct {
	r float64
}

func (c *Circle) area() float64 {
	// c 是指针, 标准方式应该是 *c
	// return 3.14 * (*c).r * (*c).r
	return 3.14 * c.r * c.r
}

func main() {
	var c Circle
	c.r = 4.0
	// res := (&c).area()
	res := c.area()
	fmt.Println("面积:", res)
}
  • 虽然 c 的类型是 *Circle(指针),但 Go 允许直接写 c.r编译器会自动解引用**,等价于 (*c).r**
  • 同样Go 会自动将 c.area() 转换为 (&c).area(),前提是 c 是可寻址的(比如变量,不是临时值)

3. 方法集

Golang方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则

  • 类型 T 方法集包含全部 receiver T 方法。
  • 类型 *T 方法集包含全部 receiver T + *T 方法。
  • 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
  • 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
  • 不管嵌入 T 或 *T,*S 方法集总是包含 T + *T 方法。

用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。

Go 语言中内部类型方法集提升的规则:类型 T 方法集包含全部 receiver T 方法

type T struct {
    int
}

func (t T) test() {
    fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
}

func main() {
    t1 := T{1}
    fmt.Printf("t1 is : %v\n", t1) // t1 is : {1}
    t1.test() 
}

类型 *T 方法集包含全部 receiver T + *T 方法

type T struct {
    int
}

func (t T) testT() {
    fmt.Println("类型 *T 方法集包含全部 receiver T 方法。")
}
func (t *T) testP() {
    fmt.Println("类型 *T 方法集包含全部 receiver *T 方法。")
}

func main() {
    t1 := T{1}
    t2 := &t1
    fmt.Printf("t2 is : %v\n", t2) // t2 is : &{1}
    t2.testT()
    t2.testP()
}

给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。

这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。

type S struct {
    T
}

type T struct {
    int
}

func (t T) testT() {
    fmt.Println("如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。")
}

func main() {
    s1 := S{T{1}}
    s2 := &s1
    fmt.Printf("s1 is : %v\n", s1) // s1 is : {{1}}
    s1.testT()
    fmt.Printf("s2 is : %v\n", s2) // s1 is : &{{1}}
    s2.testT()
}

如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。

这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。

type S struct {
    T
}

type T struct {
    int
}

func (t T) testT() {
    fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法")
}
func (t *T) testP() {
    fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法")
}

func main() {
    s1 := S{T{1}}
    s2 := &s1
    fmt.Printf("s1 is : %v\n", s1) // s1 is : {{1}}
    s1.testT()
    s1.testP()
    fmt.Printf("s2 is : %v\n", s2) // s1 is : &{{1}}
    s2.testT()
    s2.testP()
}

4. 表达式

Golang 表达式 :根据调用者不同,方法分为两种表现形式:

instance.method(args...) ---> <type>.func(instance, args...)
  • 前者称为 method value,后者 method expression。
  • 两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,而 method expression 则须 显式传参
type User struct {
    id   int
    name string
}

func (self *User) Test() {
    fmt.Printf("%p, %v\n", self, self)
}

func main() {
    u := User{1, "Tom"}
    u.Test()
    
    mValue := u.Test
    mValue() // 隐式传递 receiver

    mExpression := (*User).Test
    mExpression(&u) // 显式传递 receiver
}

// 输出
0xc00000e018, &{1 Tom}
0xc00000e018, &{1 Tom}
0xc00000e018, &{1 Tom}

需要注意,method value 会复制 receiver

func (self User) Test() {
    fmt.Println(self)
}

func main() {
    u := User{1, "Tom"}
    mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。

    u.id, u.name = 2, "Jack"
    u.Test()   	// {2 Jack}

    mValue() 	// {1 Tom}
}

在汇编层面,method value 和闭包的实现方式相同,实际返回 FuncVal 类型对象。

FuncVal { method_address, receiver_copy }

可依据方法集转换 method expression,注意 receiver 类型的差异

func (self *User) TestPointer() {
    fmt.Printf("TestPointer: %p, %v\n", self, self)
}

func (self User) TestValue() {
    fmt.Printf("TestValue: %p, %v\n", &self, self)
}

func main() {
    u := User{1, "Tom"}
    fmt.Printf("User: %p, %v\n", &u, u)

    mv := User.TestValue
    mv(u)

    mp := (*User).TestPointer
    mp(&u)
	
    // *User 方法集包含 TestValue。签名变为 func TestValue(self *User)。实际依然是 receiver value copy
    mp2 := (*User).TestValue 
    mp2(&u)
}
// 输出
User: 0xc42000a060, {1 Tom}
TestValue: 0xc42000a0a0, {1 Tom}
TestPointer: 0xc42000a060, &{1 Tom}
TestValue: 0xc42000a100, {1 Tom}

如果不明白,那么将方法 “还原” 成函数,就容易理解下面的代码了

type Data struct{}

func (Data) TestValue() {}

func (*Data) TestPointer() {}

func main() {
    var p *Data = nil
    p.TestPointer()

    (*Data)(nil).TestPointer() // method value
    (*Data).TestPointer(nil)   // method expression

    // p.TestValue()            // invalid memory address or nil pointer dereference

    // (Data)(nil).TestValue()  // cannot convert nil to type Data
    // Data.TestValue(nil)      // cannot use nil as type Data in function argument
}

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【GoLang】的内容,请持续关注我 !!

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐