方法
个人理解:函数只是为了实现某一功能,而方法是针对某一数据结构实现特定的方法
Go语言的类型方法本质上就是一个函数
- 方法的接收者类型并非一定要是struct类型,type定义的类型别名,slice,mao,channel,func都可以
- struct和方法不一定要在一个文件,但要在一个包中
//类型方法接收者是值类型
func (t TypeName) 方法名(参数列表) (返回参数) {
函数体
}
//接收者是指针类型
func (t *TypeName) 方法名(参数列表) (返回参数) {
函数体
}
//TypeName为命名类型的类型名
//首字母大写,表示外部包可见(暴露的,共有的)
普通写法
package main
import "fmt"
type person struct {
name string
age int
}
func newPerson(_name string, _age int) *person {
p := person{name: _name, age: _age}
return &p
}
func (p person) sayHello(word string) {
fmt.Println(p.name, "say:", word)
}
func main() {
p := newPerson("messi", 20)
fmt.Printf("%T\n", p)
p.sayHello("xavi")
}
值接收者和指针接收者
也就是 传值和传址
package main
import "fmt"
type person struct {
name string
age int
}
func newPerson(_name string, _age int) *person {
p := person{name: _name, age: _age}
return &p
}
//会发生类似C++拷贝构造函数的事情
func (p person) sayHello(word string) {
fmt.Println(p.name, "say:", word)
}
//传值
func (p person) passValue() {
p.age++
}
//传址
func (p *person) passAddress() {
p.age++
}
func main() {
p := newPerson("messi", 20)
fmt.Printf("%T\n", p)
p.sayHello("xavi")
p.passValue()
fmt.Println(p)
p.passAddress()
fmt.Println(p)
}
类似手动实现深拷贝
之前提到过slice底层是数组,map底层是hash,二者都包含了指向底层数据结构的指针,所以可能会出现类似c++中通过默认拷贝构造函数产生的浅拷贝问题
问题举例1
package main
import (
"fmt"
)
/*
虽然函数中是值拷贝,但是由于切片由指向底层数组的指针
所以,对s的修改会影响到实参
*/
func changeData(s []string) {
if len(s) >= 0 {
s[0] = "222"
}
}
func main() {
data := []string{"messi", "xavi", "111"}
fmt.Println(data)
changeData(data)
fmt.Println(data)
/*
[messi xavi 111]
[222 xavi 111]
*/
}
解决方法
package main
import (
"fmt"
)
type Person struct {
name string
age int8
dreams []string
}
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}
//不修改实参的正确写法
/*
类似于C++中
手动申请一样大的空间,然后拷贝数据
*/
func (p *Person) SetDreams_2(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}
func main() {
p1 := Person{name: "小王子", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.SetDreams(data)
// 你真的想要修改 p1.dreams 吗?
data[1] = "不睡觉"
fmt.Println(p1.dreams) // ?
}
方法调用
一般调用
TypeInstanceName.MethodName(ParamList)
TypeInstanceName为类型实例名或指向实例的指针变量名
示例如下
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func main() {
//通过指向实例类型的指针调用
var t = &T{}
t.Set(2)
fmt.Printf("t.Get(): %v\n", t.Get())
//通过实例类型名调用
var t2 = T{}
t2.Set(1)
fmt.Printf("t2.Get(): %v\n", t2.Get())
}
方法值
变量x的静态类型是T,M是T类型的一个方法,x.M被称为方法值。x.M是一个函数类型变量,可以赋值给其他变量,并向普通的函数名一样使用,例如
f := x.M
f(args...)
其等价于
x.M(args...)
完整的例子如下:
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func (t *T) Print() {
fmt.Printf("%p, %v, %d\n", t, t, t.a)
}
func main() {
//通过指向实例类型的指针调用
var t = &T{}
f := t.Set
f(2)
t.Print()
f(3)
t.Print()
//通过实例类型名调用
var t2 = T{}
f = t2.Set
f(2)
t2.Print()
f(3)
t2.Print()
/*
0x140000160a8, &{2}, 2
0x140000160a8, &{3}, 3
0x140000160d0, &{2}, 2
0x140000160d0, &{3}, 3
*/
}
方法表达式
方法表达式相当于是将上面提到的一般调用的方法转为普通的函数调用,接收者作为函数的参数进行传递,如下的方法调用方式都能起到一样目的
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func (t *T) Print() {
fmt.Printf("%p, %v, %d\n", t, t, t.a)
}
func main() {
t := T{a: 1}
//普通方法调用
fmt.Printf("t.Get(): %v\n", t.Get())
//方法表达式调用
fmt.Printf("(T).Get(t): %v\n", (T).Get(t))
//方法表达式调用
f1 := T.Get
fmt.Printf("f1(t): %v\n", f1(t))
//方法表达式调用
f2 := (T).Get
fmt.Printf("f2(t): %v\n", f2(t))
//如下方法表达式调用也是等价的
fmt.Println("part 2")
(*T).Set(&t, 3)
f3 := (*T).Get
fmt.Printf("f3(&t): %v\n", f3(&t))
}
t.Get(): 1
(T).Get(t): 1
f1(t): 1
f2(t): 1
part 2
f3(&t): 3
方法集
先上结论,将接收者为值类型T的方法集合记为S,将接收者为指针类型* T的方法集合记为* S,则类型的方法集总结如下:
- T类型的方法集是S
-
- T类型的方法集是S和* S
这两句话该如何理解呢?
- T类型的方法集是S和* S
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func main() {
t := T{a: 1}
fmt.Printf("t.Get(): %v\n", t.Get()) //1
t.Set(2)
fmt.Printf("t.Get(): %v\n", t.Get()) //2
t1 := &T{a: 1}
fmt.Printf("t1.Get(): %v\n", t1.Get()) //1
t1.Set(2)
fmt.Printf("t1.Get(): %v\n", t1.Get()) //2
}
通过上面的代码可以看出,Get方法的接收者是值类型,Set方法的接收者是指针类型,但是不管是值类型的t还是指针类型的t1都可以完成对两个方法的调用,这似乎与上面说的 T类型的方法集是S相违背,其实并不是。
这是因为编译器在编译期间能够识别出这种调用关系,做了自动的转换。t.Set()使用值类型实例调用指针接收者的方法,编译器会自动将其装换为(&a).Set(),而t1使用指针类型调用Get方法时,也会被转为
(*t1).Set的形势
既然这里不论是什么类型的实例,都可以完成对两个方法的调用,上面的对方法集的总结岂不是在放屁?非也,继续往下看(这里引用了上面的链接)
package main
import "fmt"
type INTEGER int
func (a INTEGER) Less(b INTEGER) bool {
return a < b
}
func (a *INTEGER) Add(b INTEGER) {
*a += b
}
type LessAdder interface {
Less(INTEGER) bool
Add(INTEGER)
}
func main() {
var a INTEGER = 100
var b LessAdder = a //这里会编译报错
b.Add(30)
fmt.Println(a)
}
编译报错的原因是:cannot use a (variable of type INTEGER) as LessAdder value in variable declaration: missing method Add (Add has pointer receiver),也就是说a没有实现方法LessAdder
。这个报错解释了为什么说 T类型的方法集是S
再看下一段代码
package main
import "fmt"
type INTEGER int
func (a INTEGER) Less(b INTEGER) bool {
return a < b
}
func (a *INTEGER) Add(b INTEGER) {
*a += b
}
type LessAdder interface {
Less(INTEGER) bool
Add(INTEGER)
}
func main() {
var a INTEGER = 100
var b LessAdder = &a //类型是INTEGER
b.Add(30)
fmt.Printf("b.Less(30): %v\n", b.Less(30))
fmt.Println(a)
}
输出如下:
b.Less(30): false
130
可以看出此时*INTEGER实现了该接口的两个方法,佐证了上述总结的第二条
实例传递给接口时,会进行严格的方法集校验
下面来看另外两种方法调用方式时的方法集处理
值调用和表达式调用的方法集
使用类型字面量进行测试
package main
type Data struct {
}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
//Data的方法集只有TestValue,而*Data全都有
func main() {
//值调用
(&Data{}).TestPointer()
(&Data{}).TestValue()
(Data)(struct{}{}).TestValue()
(Data)(struct{}{}).TestPointer() //报错:cannot call pointer method TestPointer on Data
//表达式调用
Data.TestValue(Data{})
Data.TestPointer() //报错:cannot call pointer method TestPointer on Data
}
结论:通过类型字面量显示地进行值调用和表达式调用时,不会自动转换
通过类型实例进行测试
package main
type Data struct {
}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
//Data的方法集只有TestValue,而*Data全都有
var a Data = struct{}{}
func main() {
//表达式调用
Data.TestValue(a)
(*Data).TestPointer(&a)
Data.TestPointer(&a) //报错:cannot call pointer method TestPointer on Data
(*Data).TestValue(&a)
//值调用
f := a.TestValue
f()
y := (&a).TestValue //转化为a.TestValue
y()
g := a.TestPointer //转化为(&a).TestPointer
g()
x := (&a).TestPointer
x()
}
结论:通过实例进行调用时,如果通过表达式调用则不会进行转化,如果通过值调用,则会进行转换