【Go】Go语言中的方法


一、方法定义

一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

Golang 方法特性:

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

所有给定类型的方法属于该类型的方法集。

方法定义:

func (recevier type) methodName(参数列表)(返回值列表){}

参数和返回值可以省略。方法可以没有参数,也可以没有返回值。

接收者可以是一个值或者一个指针。


二、接收者为值和指针类型的区别

接收者为值类型或指针类型,决定了调用方法时,方法对实例是值操作还是指针操作。

  • 若接收者为值类型,那么无论用值还是指针调用该方法,方法操作的都是对象的副本。
  • 若接收者为指针类型,那么无论用值还是指针调用该方法,方法操作的都是对象的指针。

以下我们通过实例来说明这一点。

定义一个结构体类型和该类型的一个方法,并使用值调用和指针调用的实例:

package main

import (
	"fmt"
)

//结构体
type User struct {
	Name  string
	Email string
}

//方法
func (u User) Notify() { //接收者是User类型的一个值
	fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
	// 值调用方法
	u1 := User{"golang", "golang@golang.com"}
	u1.Notify()
	// 指针调用方法
	u2 := User{"go", "go@go.com"}
	u3 := &u2
	u3.Notify()  //这两行可以直接写成(&u2).Notify()
}

输出结果:

golang : golang@golang.com 
go : go@go.com 

解释: 首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Notify,该方法的接受者是一个 User 类型的值。要调用 Notify 方法我们需要一个 User 类型的值或者指针。

当方法的接受者是值类型而我们使用指针调用方法时,Go会自己调整使得调用可以被执行。但是要注意!当方法定义中的接受者不是一个指针,而是值类型时,该方法操作对应接受者的值的副本(意思就是即使你使用了实例的指针调用函数,但是方法的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作。

我们修改 Notify 方法,让它的接受者使用指针类型:

package main

import (
	"fmt"
)

//结构体
type User struct {
	Name  string
	Email string
}

//方法
func (u *User) Notify() {
	fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
	// 值调用方法
	u1 := User{"golang", "golang@golang.com"}
	u1.Notify()
	// 指针调用方法
	u2 := User{"go", "go@go.com"}
	u3 := &u2
	u3.Notify()
}

输出结果:

golang : golang@golang.com 
go : go@go.com 

注意:当方法的接受者类型是指针时,即使用值来调用方法,方法内部也是对指针的操作。

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

package main

import "fmt"

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() //值调用
	p.ValueTest() //指针调用

	//都是地址引用
	d.PointerTest() //值调用
	p.PointerTest() //指针调用
}

输出结果:

Data: 0xc00000a198
Value: 0xc00000a1b8
Value: 0xc00000a1d0
Pointer: 0xc00000a198
Pointer: 0xc00000a198

总结:

是值引用还是地址引用取决于 定义 方法时 接收者的类型
如果定义时,接收者是值类型,那么调用该方法时,操作的就是值的副本。
如果定义时,接收者是指针类型,那么调用该方法时,操作的就是指针。
与调用方法无关(与用实例的值还是指针来调用方法无关)。


三、普通函数与方法的区别

普通函数不如方法的调用方式如此灵活,普通函数定义参数时是什么类型,调用时就用什么类型。

1.对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。(从上一节的实例中,我们也可以洞察到这一点)

2.但是,对于普通函数,形参为值类型时,调用时应用值类型的数据,而不能将指针类型的数据直接传递给它,反之亦然。

package main

//普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)

import (
	"fmt"
)

//1.普通函数
//接收值类型参数
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))
	//函数的参数为值类型,则不能直接将指针作为参数传递
	//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))
	//同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
	//fmt.Println("pointerIntTest:", pointerIntTest(b))
	//compile error:cannot use b (type int) as type *int in function argument
}

//2.方法
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()

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

func main() {
	structTestValue()
	structTestFunc()
}

输出结果:

valueIntTest: 12
pointerIntTest: 15
hello world
hello world
hello golang
hello golang

四、匿名字段

1.Golang匿名字段 :可以像字段成员那样访问匿名字段方法,编译器负责查找。

结构体有匿名字段,如果在结构体上直接调用定义在其 匿名结构体字段 上的方法,也是可行的。编译器会自己查找。

实例:

package main

import "fmt"

type User struct {
	id   int
	name string
}

type Manager struct {
	User //结构体的匿名字段,没有字段名,只有类型
}

func (self *User) ToString() string { //在结构体User上定义一个方法ToString,receiver = &(Manager.User)
	return fmt.Sprintf("User: %p, %v", self, self)
}

func main() {
	m := Manager{User{1, "Tom"}} //实例化一个Manager结构体
	fmt.Printf("Manager: %p\n", &m)
	fmt.Println(m.ToString()) //在结构体Manager上调用结构体User的方法
}

输出结果:

Manager: 0xc000096060
User: 0xc000096060, &{1 Tom}

2.通过匿名字段,可 获得和继承类似的复用能力 。依据编译器查找次序,只需在外层定义 同名方法,就可以实现 “override”。

实例:

package main

import "fmt"

type User struct {
	id   int
	name string
}

type Manager struct {
	User
	title string
}

func (self *User) ToString() string {
	return fmt.Sprintf("User: %p, %v", self, self)
}

func (self *Manager) ToString() string {
	return fmt.Sprintf("Manager: %p, %v", self, self)
}

func main() {
	m := Manager{User{1, "Tom"}, "Administrator"}

	fmt.Println(m.ToString())

	fmt.Println(m.User.ToString())
}

输出结果:

Manager: 0xc000072480, &{{1 Tom} Administrator}
User: 0xc000072480, &{1 Tom}

注意,结构体 Manager 与其儿子结构体 User 上都定义了方法 ToString() (同名方法),这就拥有了和继承类似的复用能力。


五、方法集

1. 方法集规则

一个类型的方法集可以理解为该类型结构体变量可以调用的方法的全体。

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

  1. 类型 T 的方法集包含全部 receiver T 方法。
  2. 类型 *T 的方法集包含全部 receiver T + *T 方法。
  3. 如类型 S 包含匿名字段 T,则 S 和 *S 的方法集包含 T 方法。
  4. 如类型 S 包含匿名字段 *T,则 S 和 *S 的方法集包含 T + *T 方法。
  5. 不管嵌入 T 或 *T,*S 的方法集总是包含 T + *T 方法。

如果接收者是值类型 T ,那么方法对接受者就是值操作。所以 值类型 T 的方法集 包含的是所有接受者为值类型 T 的方法。
如果接收者是指针类型 *T ,那么方法对接受者就是指针操作。所以 指针类型 *T 的方法集包含的是所有接受者为值类型 T 和 指针类型 *T 的方法。

2. Go 语言中内部类型方法集提升的规则 - 理解方法集规则的实例

实例一 :类型 T 的方法集包含全部 receiver T 方法。

这个很好理解,类型为 T 的变量可以调用所有接收者为 T 的方法。

例如,在下例中,t1 为 T 类型的变量,所以 t1 可以调用所有定义在类型 T 上的方法,比如 test() 。

package main

import (
	"fmt"
)

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.test()
}

输出结果:

t1 is : {1}
类型 T 方法集包含全部 receiver T 方法。

实例二 :类型 *T 的方法集包含全部 receiver T + *T 方法。

意思是,类型为 *T 的变量可以调用所有接收者为 T 或 *T 的方法。

例如,在下例中,t2 为 *T 类型的变量,所以 t2 可以调用所有定义在类型 T 或 *T 上的方法,比如定义在类型 T上的方法 testT() 和定义在类型 *T上的方法 testP()。

package main

import (
	"fmt"
)

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.testT()
	t2.testP()
}

输出结果:

t2 is : &{1}
类型 *T 方法集包含全部 receiver T 方法。
类型 *T 方法集包含全部 receiver *T 方法。

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

意思是,当类型 S 包含 匿名字段 T时,类型为 S 和 *S 的变量可以调用所有接收者为 T 的方法。

下例中,s1 是类型为 S 的变量,s2 是类型为 *S 的变量,它们都可以调用定义在类型 T 上的方法 testT()。

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

package main

import (
	"fmt"
)

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.testT()   //S方法集包含T方法
	fmt.Printf("s2 is : %v\n", s2)
	s2.testT()   //*S方法集包含T方法
}

输出结果:

s1 is : {{1}}
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
s2 is : &{{1}}
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。

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

意思是,当类型 S 包含 匿名字段 *T 时,类型为 S 和 *S 的变量可以调用所有接收者为 T 和 *T 的方法。

下例中,s1 是类型为 S 的变量,s2 是类型为 *S 的变量,它们都可以调用定义在类型 T 上的方法 testT() 和定义在 *T 上的方法 testP()。

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

package main

import (
	"fmt"
)

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.testT() //S方法集包含T方法
	s1.testP() //S方法集包含*T方法
	fmt.Printf("s2 is : %v\n", s2)
	s2.testT() //*S方法集包含T方法
	s2.testP() //*S方法集包含*T方法
}

输出结果:

s1 is : {{1}}
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法
s2 is : &{{1}}
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法

3. 一个说明

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


六、表达式

1. 两种表达式

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

1. instance.method(args...) 
2. <type>.func(instance, args...)

前者称为 method value,后者 method expression

两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,而 method expression 则须显式传参。

实例:

package main

import "fmt"

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
}

输出结果:

0xc000004078, &{1 Tom}
0xc000004078, &{1 Tom}
0xc000004078, &{1 Tom}

2. method value

需要注意,method value 会复制 receiver。实例:

package main

import "fmt"

type User struct {
	id   int
	name string
}

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()

	mValue()
}

输出结果:

{2 Jack}
{1 Tom}

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

FuncVal { method_address, receiver_copy }

3. method expression

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

package main

import "fmt"

type User struct {
	id   int
	name string
}

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)

	mp2 := (*User).TestValue // *User方法集包含TestValue。签名变为func TestValue(self *User)。实际依然是receiver value copy。
	mp2(&u)
}

输出结果:

User: 0xc000004078, {1 Tom}
TestValue: 0xc0000040a8, {1 Tom}
TestPointer: 0xc000004078, &{1 Tom}
TestValue: 0xc0000040f0, {1 Tom}

4. 将方法 “还原” 成函数

将方法 “还原” 成函数,就容易理解下面的代码了。

package main

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
}

七、自定义error

1. 抛异常和处理异常

package main

import "fmt"

// 系统抛
func test01() {
	a := [5]int{0, 1, 2, 3, 4}
	a[1] = 123
	fmt.Println(a)
	//a[10] = 11
	index := 10
	a[index] = 10
	fmt.Println(a)
}

func getCircleArea(radius float32) (area float32) {
	if radius < 0 {
		// 自己抛
		panic("半径不能为负")
	}
	return 3.14 * radius * radius
}

func test02() {
	getCircleArea(-5)
}

//
func test03() {
	// 延时执行匿名函数
	// 延时到何时?(1)程序正常结束   (2)发生异常时
	defer func() {
		// recover() 复活 恢复
		// 会返回程序为什么挂了
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	getCircleArea(-5)
	//延时函数在这里进行,然后程序正常结束
	fmt.Println("这里有没有执行") //没有执行
}

func test04() {
	test03()
	//test03()正常结束,再执行以下这句
	fmt.Println("test04")
}

func main() {
	test04()
}

输出结果:

半径不能为负
test04

2. 返回异常

package main

import (
	"errors"
	"fmt"
)

func getCircleArea(radius float32) (area float32, err error) {
	if radius < 0 {
		// 构建个异常对象
		err = errors.New("半径不能为负")
		return //返回异常
	}
	area = 3.14 * radius * radius
	return
}

func main() {
	area, err := getCircleArea(-5)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(area)
	}
}

输出结果:

半径不能为负

3. 自定义error

package main

import (
	"fmt"
	"os"
	"time"
)

type PathError struct {
	path       string
	op         string
	createTime string
	message    string
}

func (p *PathError) Error() string {
	return fmt.Sprintf("path=%s \nop=%s \ncreateTime=%s \nmessage=%s", p.path,
		p.op, p.createTime, p.message)
}

func Open(filename string) error {

	file, err := os.Open(filename)
	if err != nil {
		return &PathError{
			path:       filename,
			op:         "read",
			message:    err.Error(), //调用Error()方法
			createTime: fmt.Sprintf("%v", time.Now()),
		}
	}

	defer file.Close()
	return nil
}

func main() {
	//调用Open函数,返回错误信息&PathError
	err := Open("/Users/5lmh/Desktop/go/src/test.txt")
	switch v := err.(type) { //.(type)理解:获取接口实例实际的类型指针,以此调用实例所有可调用的方法,包括接口方法及自有方法。
	//需要注意的是该写法必须与switch case联合使用,case中列出实现该接口的类型。
	case *PathError:
		fmt.Println("get path error,", v)
	default:

	}

}

输出结果:

get path error, path=/Users/5lmh/Desktop/go/src/test.txt 
op=read 
createTime=2022-02-17 18:27:33.6585795 +0800 CST m=+0.005293101 
message=open /Users/5lmh/Desktop/go/src/test.txt: The system cannot find the path specified.

参考链接

  1. 方法定义
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值