【GoLang# 8】:方法

📃个人主页: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 接收者类型
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如果程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
值接收者 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
一些说明
- Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法
- 比如:如果一个变量实现了
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 函数
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
- 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。
- 调用方式
- 函数:函数名(实参列表)
- 方法:变量.方法名(实参列表)
普通函数
//接收值类型参数的函数
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】的内容,请持续关注我 !!

更多推荐



所有评论(0)