go学习记录——第十三天

方法

方法是什么

Golang 中,结构体就像是类的一种简化形式,那么面向对象的程序员可能会问类的方法在哪里呢?(反正我是不会问,咱不会Java)在 Golang 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Golang 方法是作用在接受者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。

接受者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数,可以是 int bool string 或 数组的别名类型。但是接收者不能是一个接口类型(参考第十一章),因为接口是一个抽象定义,但是方法却是具体实现的;如果这样做会引发一个编译错误:invalid receiver type...

最后接收者不能是一个指针类型,但它可以是任何其他允许类型的指针。

好的,到这里还是没明白接收者到底能干啥不能干啥。问了下AI然后自己总结吧

首先可以作为接收者的类型

  1. 结构体类型
  • 最常见的接收者类型。
  • 可以定义方法来操作该结构体的字段。
  1. 非结构体类型(如基本类型、数组、切片等)
  • 通过嵌入结构体或定义新的类型别名来实现类似结构体的方法集。
  1. 命名类型
  • 使用type关键字创建的自定义类型。
  • 包括基于内置类型的别名。
  1. 指针类型
  • 指向结构体或其他类型的指针也可以作为接收者。
  • 使用指针接收者可以在方法内部修改原始值。
  1. 函数类型
  • 虽然不太常见,但理论上函数类型也可以有自己的方法。

不可以作为接收者的类型

  1. 接口类型
  • 接口本身不能直接定义方法,它们只是方法签名的集合。
  • 方法必须绑定到具体的实现该接口的具体类型上。
  1. 字面量类型
  • intstring这样的基础数据类型的字面量形式不能单独作为接收者。
  1. 临时匿名结构体
  • 直接在函数调用中构造的匿名结构体实例不能用作方法的接收者。

代码示例:

正确示例:结构体作为接收者

1
2
3
4
5
6
7
type Rectangle struct {
width, height float64
}

func (r Rectangle) Area() float64 {
return r.width * r.height
}

错误示例:接口作为接收者

1
2
3
4
5
6
7
8
type Shape interface {
Area() float64
}

// 下面的代码是非法的,因为Shape是一个接口类型
func (s Shape) DoubleArea() float64 {
return s.Area() * 2
}

好的,继续

一个类型加上它的方法等价于面向对象中的一个类。一个区别是,在 Golang 中,类型的代码和绑定在上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是必须在同一个包中。

类型 T (或 *T)上的所有方法的集合叫做类型 T (或 *T)的方法集(method set)

因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有个给定名称的方法。但是如果基于接收者类型,石油重载的:具有同样名字的方法可以在两个或多个不同的接收者上存在,比如在同一个包里是被允许的。

举个例子来说明

假设我们有两个结构体CircleRectangle,并且我们想为它们都定义一个计算面积的方法Area

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
Width, Height float64
}

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

在这个例子中:

  • Circle类型有一个名为Area的方法。
  • Rectangle类型也有一个名为Area的方法。

尽管这两个方法的名称相同,但由于它们的接收者类型不同(一个是Circle,另一个是Rectangle),因此它们被视为两个独立的方法,而不是重载关系。

别名类型没有原始类型上已经定义过的方法。

这里需要特别说明一下,别名类型和原始类型是相互独立的,只会继承结构不会继承方法。

如果想要继承方法是需要手动进行的

就比如我们原始int类型给了一个方法

1
2
3
func (i int) Double() int {
return i * 2
}

直接创建别名类型 MyInt 是这样

1
type MyInt int

然后我们手动给别名添加方法如下

1
2
3
func (mi MyInt) Double() MyInt {
return mi * 2
}

定义方法的格式如下

1
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

在方法名前,func 关键字后的口号中指定 receiver

如果 recvreceiver 的实例,Method1 是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:recv.Method1()

如果 recv 是一个指针,Go会自动解引用。

如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:

1
func (_ receiver_type) methodName(parameter_list) (return_value_list) {...}

recv 就像是面向对象语言中的 thisself ,但是 Go 中并没有这两个关键字。随个人喜好,可以使用 thisself 作为 receiver 的名字(虽然但是,我完全不觉得会有人想用这个作为receiver的名字)。下面是一个结构体上的简单方法和例子:

示例 method1.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type TwoInts struct {
a int
b int
}

func main() {
two1 := new(TwoInts)
two1.a = 12
two1.b = 34
fmt.Printf("The sum is: %d\n", two1.AddThem())
fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))

two2 := TwoInts{56, 78}
fmt.Printf("The sum is: %d\n", two2.AddThem())
}

func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}

func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}

输出结果

1
2
3
The sum is: 46
Add them to the param: 66
The sum is: 134

下面是非结构体类型上方法的例子

示例method2.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

type IntVector []int

func (v IntVector) sum() (s int) {
for _, x := range v {
s += x
}
return
}

func main() {
fmt.Println(IntVector{1, 2, 3}.sum())// Output: 6
}

在练习中有个这个题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 下面这段代码有什么错
package main

import "container/list"

func (p *list.List) Iter() {
// ...
}

func main() {
lst := new(list.List)
for _= range lst.Iter() {
}
}

错误就是我们试图在list类型上定义方法,但是我们的list来源不是在这个包内,所以肯定会报错。同样的,我们在给int float32 或类似这些的类型上定义方法时,就会报类似的错误(虽然但是,书上这句话和这个题目好像没有半点关系啊)

1
cannot define new methods on non-local type int

比如想在 time.Time 上定义如下方法:

1
2
3
func (t time.Time) first3Chars() string{
return time.LoccalTime().String()[0:3]
}

类似的,在其他的或非本地的包里定义,都会得到上面的错误。

但是有一个间接的方法,可以先定义该类型(比如:intfloat32(64))的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效(别名真神奇哦,完全隔离,宛如两个生殖隔离的物种)

示例method_on_time.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"time"
)

type myTime struct {
time.Time // anonymous field
}

// 获取完整时间的字符串
func (t myTime) fullTime() string {
return t.Time.Format("Mon Jan 2 15:04:05 MST 2006")
}

// 获取时间字符串的前三个字符
func (t myTime) first3Chars() string {
return t.Time.String()[0:3]
}

func main() {
m := myTime{time.Now()}

fmt.Println("full time now:", m.fullTime()) // 使用 new method 输出完整时间
fmt.Println("First 3 chars:", m.first3Chars()) // 使用 first3Chars 方法
}

输出示例

1
2
full time now: Mon Dec 9 10:33:27 CST 2024
First 3 chars: 202

这边需要说明一下,当我们用 time.Now() 会调用系统时间,所以我们的第二个输出直接就是202,第一个输出因为我们用format 把时间格式化了,所以才能显示我们想要的这个。

函数和方法的区别

函数将变量作为参数: Function1(recv)

方法在变量上被调用: recv.Method1()

在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)

**不要忘记 Method1() 后边的括号 () ,否则会引发编译器报错: method recv.Method1 is not an expression,must be called **

接收者必须有一个显式的名字且这个名字可以在方法中被调用。

receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。

在 Go 中,(接收者)类型关联的方法不写在类型结构体里面,就像类那样;耦合更加宽松;类型和方法之间关联由接收者来建立。

方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是相互独立的。

感觉讲的好绕啊,我自己捋一遍吧。

总结

可以直接看我这个,前面的不用看也可以(我自己是这么觉得的🤣)

前提:函数和放啊都是用于封装一组操作的代码块,但它们之间存在一些关键区别,尤其是设计接收者时

函数(Function)

  1. 定义方法

    • 函数使用 func 关键字定义
    • 不需要绑定到任何特定的类型
  2. 调用方法

    • 直接通过函数名和参数列表调用
    • 例如: Add(3, 8)
  3. 接收者

    • 函数没有接收者
    • 函数独立于任何特定的数据结构
  4. 作用域

    • 可以在整个包内或外部包中被访问(取决于可见性)
  5. 示例

    1
    2
    3
    func Add(a int, b int) int {
    return a + b
    }

方法(Method)

  1. 定义方式

    • 方法也使用 func 关键字定义
    • 必须有一个接收者参数,该参数指定了方法关联的类型
  2. 调用方式

    • 通过类型和 . 运算符来调用
    • 例如: num.Add(6)
  3. 接收者

    • 方法有一个或多个接收者,可以是结构体类型、非结构体类型(如基本数据类型、数组、切片等)的别名或接口
    • 接收者可以是任何值类型或指针类型
    • 值接收者会在方法调用时复制一份数据(两者相互独立,保证原始数据的稳定性,但可能会影响性能),指针接收者会直接操作原始数据
  4. 作用域

    • 方法通常定义在其关联类型的内部,但也可以在外部定义(只要接收者类型是导出的)
  5. 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type MyInt int

    func (mi MyInt) Double() MyInt {
    return mi * 2
    }

    // 或者使用指针接收者
    func (mi *MyInt) Increment() {
    *mi++
    }

接收者的作用

  • 封装:方法允许我们将相关的行为绑定到特定的类型上,从而实现更好的封装和组织代码,
  • 修改状态:通过指针接收者,方法可以直接修改其关联实例的状态
  • 多态性:当类型实现了某一个接口的所有方法时,该类型的实例就可以被视为该接口类型,从而实现多态
特性 函数 方法
定义 func 关键字,无接收者 func 关键字,有接收者
调用 直接调用 通过实例调用
接收者 有(值或指针)
作用域 全局或包内 类型相关联
状态修改 不能直接修改调用者状态 可以通过指针接收者修改

方便对比搞个脑图(试了试ai做的)

image

指针或值作为接收者

鉴于性能,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。

如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。

示例pointer_value.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type B struct {
thing int
}

func (b *B) change() {
b.thing = 1
}

func (b B) write() string {
return fmt.Sprint(b)
}
func main() {
var b1 B // b1 is a value type
b1.change()
fmt.Println(b1.write())

b2 := new(B) // b2 is a pointer type
b2.change()
fmt.Println(b2.write())
}

输出结果

1
2
{1}
{1}

上面的例子中 pointer_value.gochange()接受了以一个指向 B 的指针,并改变了它的内部成员;write() 通过拷贝接受了 B 的值并只输出 B 的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是否在职镇上调用方法,Go 替我们做了这些事情,b1 是值而 b2 是指针,方法都支持运行了。

捋一下,b1 虽然是一个值但是编译器会自动的将其变为指针进行传递(隐式)而 b2 则是直接进行指针传递(显式),在Go 中,在对方法进行操作的时候我们都是需要进行指针类型的转换,自身已经是则会显式直接进行操作(b2)而自身是值则会直接由编译器进行一个改变并传递指针(b1

试着在 write() 中改变接收者 b 的值:将会看到它可以正常编译,但是开始的 b 没有被改变。

首先我们知道,方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 Point3 的值来做计算:

1
2
3
4
5
6
type Point3 struct {
x, y, z float64
}
func (p Point3) Abs() float64{
return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z)
}

这样的操作消耗会有点大,因为 Point3 是作为值传递给方法,因此传递的是它的拷贝,这种是可以的,同时也可以是在指向这个类型的指针上调用此方法(会自动解引用)。

假设 p3 定义为一个指针:p3 := &Point{3, 4, 5}

可以使用 p3.Abs() 来代替 (*p3).Abs()

像前面的示例(method1.go)中接受者类型是 *TwoInts 的方法 AddThem() ,它能在类型 TwoInts 的值上被调用,这是自动间接发生的。因此 two2.AddThem 可以替代 (&two2).AddThem()

在值和指针上调用方法:

可以有连接到类型的方法,也可以有连接到指针的方法。

但是这没关系:对于类型 T ,如果在 \*T 上存在方法 Meth() ,并且 t 是这个类型的变量,那么 t.Meth() 会被自动转换为 (&t).Meth()

指针方法和脂肪啊都可以在指针或非指针上被调用,如下面的例子所述,类型 List 在值上有一个方法 Len() ,在指针上有一个方法 Append() ,但是可以看到这两个方法都可以在两种类型的变量上被调用。

示例methodset1.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type List []int

func (l List) Len() int {
return len(l)
}
func (l *List) Append(val int) {
*l = append(*l, val)
}

func main() {
// 值
var lst List
lst.Append(1)
fmt.Printf("%v (len: %d)\n", lst, lst.Len()) // Output: [1] (len: 1)
// 指针
plst := new(List)
plst.Append(2)
fmt.Printf("%v (len: %d)", plst, plst.Len()) // Output: &[2] (len: 1)
}

go学习记录——第十三天
https://www.lx02918.ltd/2024/12/07/go-study-thirteenth-day/
作者
Seth
发布于
2024年12月7日
许可协议