类型方法和函数
概述
下面直接进入主题
在go中可以自定义类型,比如type N int,可以为这个类型添加各种方法,可以看出方法具有接收者receiver。而函数则是类似C语言的一般函数,给定一个参数进行某种操作。00
即方法是指哪个类型的方法,而函数是无依赖自由的。
type N int func(n N)value(){..} func(n *N)pointer(){...}
如上所示,给N添加了两个方法,为了能牢记区别可以将上面两个方法等价于函数对待,等价于:
func value(n N)
func pointer(n *N)
但是N和*N毕竟是不同的,它们到底有什么区别呢?下面逐一分析
值调用
type N int func (n N) value() { n++ fmt.Printf("v:%p,%v ", &n, n) } func (n *N) pointer() { *n++ fmt.Printf("p:%p,%v ", n, *n) } func main() { var a N = 25 fmt.Printf("实例地址:%p ", &a) a.value() //产生了复制 a.pointer() }
【分析】
首先产生一个N类型的a,然后调用两个方法,直觉上来说a应该不能调用pointer()方法,因为字面上看需要的是一个指针来调用。实际是可以的,因而有下面一条结论:
- 【结论】用实例值或指针类型调用方法时,编译器会自动在基础类型和指针类型之间转换
对照这个结论,可以推测,a.pointer()调用时,实际是把&a传进去了。
同理,也可以用指针来调用value()方法,只是把当前实例值传进去又复制了一份,这个意思是指虽然是用指针调用,但是编译器依然把真正的值复制一份传进去,在方法内部是另一个值,已经和调用指针没啥关系
这说明,用前缀调用时不管哪种形式都可以,只有一个最大的区别:
- 【结论】调用value()这种receiver为T的方法时,会产生复制
- 【结论】而调用pointer()这种receiver为*T的方法时,不会产生复制
这是第二条结论,可以通过各种小程序来测试
类型T调用
这种方式很少在实际代码中看到使用,但是能运行而且能加深了解。
调用方式如下(这个地方看作是函数调用更直观):
用*T说明绑定的第一个入参必须是实例地址。如果使用a则编译错误
举一反三,会不会有下面这种样式?
T.value()
T.pointer()
结果很失望,第二个调用不行!
到这里就出现一个方法集的概念。可以把基础类型认为是T,
- T具有所有receiver为T的方法
- *T具有所有receiver为T或*T的方法
这个结论说明,*T比T的覆盖范围大,对于*T来说不管什么方法都能调用,前提是入参必须为地址。
示例代码如下:
type T int func (n *T) show(s string) { fmt.Println(s) } func main() { var a T = 25 (*T).show(&a, "hello") }
【分析】
(*T)调用了show()方法,并同时传入了&a和”hello”,这个show方法此时看起来就像是个函数。后面再绑定一个实例&a,在学习C++的时候知道,调用类成员方法时隐含了一个this入参,而这里是显式的传进去。
通过上面的分析,应该明白了T和*T的区别
总结来说:
【结论】
- 调用receiver为T的方法会产生复制
- 调用receiver为*T的方法不会产生复制
- 用(*T)可以调用所有的方法,前提是绑定的必须是实例值的地址
- 用(T)只能调用receiver为T的方法,且绑定的必须是实例值
类型内嵌的方法调用
上面把单个类型分析清楚了,下面又出现一个问题,要是T有内嵌的类型怎么办?
这个其实还是归结于”递归”的概念
根据前面的分析,*T比T可调用方法的范围大,因此,如果内嵌的是一个*S,那么它就可以调用所有方法。
模型如下:
type S struct{}
type T sturct{
S
}或
type T struct{
*S
}
- 如果内嵌的是一个S,则T具备所有receiver为S的方法
- 如果内嵌的是一个*S,则T具备所有receiver为S或*S的方法(可以认为是*S的能力大)
- 如果内嵌的是一个S或*S,则*T具备所有receiver为S或*S的方法(*T的覆盖范围最大)
这里主要说内嵌*S的问题,由于是内嵌的是地址,这个地址还需要进行初始化,比如说
示例代码一
type S struct{} type T struct { *S } func (s *S) s1() {} func main() { var t T fmt.Println(t.S) //<nil> t.s1() }
【分析】
根据上面的结论可知,内嵌一个*S,则T具备receiver为S或*S的方法,现在t企图调用*S的方法s1(),由于内嵌的*S没有初始化(等于nil),但是依然调用成功了!
这说明一个nil的地址也能调用方法,至少对于当前的代码来说是可行的,IDE会给出警告。我们可以看到它能运行仅仅是因为这个方法没有引用任何的成员变量
t.s1等价于调用(*S).s1(nil),nil也可以入参调用,但是不能涉及引用成员变量的操作,也就是作为纯粹的方法操作
回到源头,简化实验代码:
示例代码二
type T struct { name string } func (t *T) t1() { fmt.Println(t.name) } func main() { var t T var p = &t p = nil p.t1() //挂了 }
【分析】这次没有好运气了,由于引用了成员变量,不能用nil的实例去引用成员值,因此这次挂掉了。
同理,地址为nil的实例更不能调用receiver为T的方法,因为这种方法首先就需要复制原值,地址为nil的地方哪会有值呢,所以马上就会挂掉
实例:sync.Mutex锁的误区
代码如下:
type data struct { sync.Mutex } func (d data) test(s string) { d.Lock() defer d.Unlock() for i := 0; i < 5; i++ { fmt.Println(s, i) time.Sleep(time.Second) } } func main() { var wg sync.WaitGroup wg.Add(2) var d data go func() { defer wg.Done() d.test("read") }() go func() { defer wg.Done() d.test("writer") }() wg.Wait() }
【分析】
开了两个go程分别执行test方法,由于data的方法为T类型,根据前面所说的,对于receiver为T类型的,会产生复制,再翻看sync.Mutex文档时,它有这么一句:A Mutex must not be copied after first use.
大概是说在第一次使用之前一定不能复制。结果上面的方法调用上来就产生了一次复制,导致锁失效!
因此,func(d data)test(s string)方法改为:func(d *data)test(s string),前面分析过,d.test()这种调用时,编译器会进行转换把&d传进去,因此就不存在成员变量复制的问题
【结论】:
内嵌sync.Mutex时
>> 如果直接内嵌,则方法(需要lock)的receiver必须为*T
>> 如果采用*sync.Mutex时,那么需要手工对它进行初始化,这个看个人选择
推荐第一种方式
实例:反射方法集
有时需要查看类型T是否实现了某个接口,前面也说过,*T的覆盖范围最大,其次是T,首先看个例子:
实现接口断言
type root interface { Log() Log2() } type S struct{} //导出的方法 func (s S) Log() { fmt.Println("log") } //导出的方法 func (s *S) Log2() { fmt.Println("log2") } func main() { var s interface{} = S{} _, ok := s.(root) fmt.Println(ok) //false var s1 interface{} = &S{} _, ok = s1.(root) fmt.Println(ok) //true methodSet(s) fmt.Println("----") methodSet(s1) } func methodSet(v interface{}) { t := reflect.TypeOf(v) for i, n := 0, t.NumMethod(); i < n; i++ { m := t.Method(i) fmt.Println(m.Name, m.Type) } }
【分析】
root接口包含两个方法,对于类型T只实现其中一个,而*T则包含所有的方法,因此,T未实现接口。
注意:反射的NumMethod()获取的是导出的方法数量,也就是首字母大写的方法
举一反三,有一个涉及接口的问题
type S interface { work() } type T struct{} func (t *T) work() { fmt.Println("work") } func main() { var s S = T{} //能不能通过编译? }
【分析】
根据方法集分析,T类型只包含receiver为T的方法,但是代码只定义了*T的方法,因此T没有实现S接口,不能通过编译!
接口接收的是值还是指针?
有时候需要知道一个接口存储的是值或是指针,采用的形式如下:
接口.(*T):判断接口是否是指针。根据上面提到的,指针比值的覆盖范围大,如果一个接口接收的是指针,那么它肯定也实现了接口,下面举个例子说明:
//接口 type Log interface { export() } //FileLog type FileLog struct { file_name string } //FileLog实现export func (log FileLog) export() { fmt.Println("log") } func main() { file_log := &FileLog{ "check_out.dat", } var log interface{} = file_log //log := Log(file_log) //另一种写法 ptr_log, ok := log.(*FileLog) fmt.Println(ok) fmt.Println(ptr_log) fmt.Printf("%p ", file_log) //0xc0000881e0 fmt.Printf("%p ", ptr_log) //0xc0000881e0 }
【分析】:log.(*FileLog)是来判断log接口上一步接收的是指针
内嵌的调试
下面再看下内嵌的形式
第一种形式 内嵌类型S
type S struct{} func (s S) S1() {} func (s *S) S2() {} type T struct { S } func (t T) T1() {} func main() { var t T methodSet(t) /* S1 func(main.T) T1 func(main.T) */ methodSet(&t) /* S1 func(*main.T) S2 func(*main.T) T1 func(*main.T) */ } func methodSet(v interface{}) { t := reflect.TypeOf(v) for i, n := 0, t.NumMethod(); i < n; i++ { m := t.Method(i) fmt.Println(m.Name, m.Type) } }
【分析】
验证了前面的说法,如果内嵌S
- T包含receiver为S的方法
- *T包含receiver为S或*S的方法
第二种形式 内嵌*S
type S struct{} func (s S) S1() {} func (s *S) S2() {} type T struct { *S } func (t T) T1() {} func (t *T) T2() {} func main() { var t T methodSet(t) /* S1 func(main.T) S2 func(main.T) T1 func(main.T) */ fmt.Println("----") methodSet(&t) /* S1 func(*main.T) S2 func(*main.T) T1 func(*main.T) T2 func(*main.T) */ } func methodSet(v interface{}) { t := reflect.TypeOf(v) for i, n := 0, t.NumMethod(); i < n; i++ { m := t.Method(i) fmt.Println(m.Name, m.Type) } }
【分析】
也验证了前面的说法,省略
选修阅读:成员方法引用
首先看个代码:
receiver为T
type N int func (n N) test() { fmt.Printf("test.n:%p,%v ", &n, n) } func main() { var n N = 100 n++ f1 := n.test //赋值给变量时,会立即计算复制该方法执行时的receiver对象 n++ f1() //101 }
【分析】
类型T的方法可以赋值给别的变量,在赋值时会立即计算当前的receiver对象值,类似于闭包封印状态,也就是赋值那一刻的状态。
因此在第二次执行n++时,对f1()的调用并未产生影响
receiver为*T
当receiver为*T的方法被引用时,仅仅复制指针,代码如下:
type N int func (n *N) test() { fmt.Printf("test.n:%p,%v ", n, *n) } func main() { var n N = 100 n++ f1 := n.test //赋值给变量时,只复制指针 n++ f1() //102 }
实例:组合继承的问题
从一个网上的问题说起,代码如下:
type People struct{} func (p *People) ShowA() { fmt.Println("showA") p.ShowB() } func (p *People) ShowB() { fmt.Println("showB") } type Teacher struct { People } func (t *Teacher) ShowB() { fmt.Println("teacher showB") } func main() { t := Teacher{} t.ShowA() }
【分析】
这是Golang的组合模式,可以实现OOP的继承。被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。
此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Techer类型的功能。
结果显示:showA showB
摘自网上的解释,这个问题很有意思而且一不小心就错了。容易把C++的那套继承方式拿来对比。现在答案已经知道,问题是这个题隐含了一个难题,
想在父类(姑且这么说)中调用一个方法,而且希望子类来重写,如果子类重写就调用子类的方法。(至少从题中可以看出是这个意思)
但是从当前的方式是行不通了,即使Teacher重写了ShowB,但是从People的视角来看是看不到的。
这个其实就是设计模式中的模板方法。想被子类重写的方法称为钩子方法。
下面是一个golang实现供参考:
package main import ( "fmt" ) type Downloader interface { Download(uri string) } type implement interface { download() save() } type template struct { implement uri string } func newTemplate(impl implement) *template { return &template{ implement: impl, } } func (t *template) Download(uri string) { t.uri = uri fmt.Print("prepare downloading ") t.implement.download() t.implement.save() fmt.Print("finish downloading ") } func (t *template) download() { fmt.Println("default download...") } func (t *template) save() { fmt.Println("default save...") } type HTTPDownloader struct { *template } func NewHTTPDownloader() Downloader { downloader := &HTTPDownloader{} template := newTemplate(downloader) downloader.template = template return downloader } /*func (d *HTTPDownloader) download() { fmt.Printf("download %s via http ", d.uri) }*/ /* 这里如果我把下面的save函数注释掉,*HTTPDownloader.save()则会调用 *template 实现的save()方法。 上层的把底层的覆盖了,这个有点用处。 */ func (*HTTPDownloader) save() { fmt.Println("http save...") } func main() { downloader := NewHTTPDownloader() downloader.Download("http://example.com/abc.zip") }
【分析】
这个代码最经典的地方在于交叉引用,template实现了implement,HTTPDownloader也实现了。就实现了子类覆盖父类,同时,template可以部分实现,HTTPDownloader也可以部分实现!
引用类型的方法
map类型
下面看下引用类型的方法,有很大不同,一般来说引用有六个类型,这里拿map和slice举例,用的频率比较高些。
对于引用来说,地址似乎没有太大意义,因为其本身已经是引用。
例子:有一个水果map,Key为水果名称,Value为产地,我们使用type来定义方便封装,代码如下:
/* type定义Fruit类型 */ type Fruit map[string]string /* 增加水果 */ func (fruit Fruit) Add(name string, region string) { fmt.Printf("%p ", fruit) fruit[name] = region } func main() { fruit := make(Fruit) fmt.Printf("%p ", fruit) fruit.Add("apple", "gz") if _, ok := fruit["apple"]; ok { fmt.Println("apple exist") //"apple exist" } }
【分析】:代码和上面的例子都不同,这里的类型本身是引用,因此就不存在复制的情况。个人觉得,引用最好不要和指针搅和在一块。比如使用new引用。
切片类型
切片也是引用类型,但实际是极度不推荐,参考下面代码:
/* 为切片类型定义方法,这种类型极不可取,基本不用! */ type Fruit []string func(fruit Fruit)Add(name string){ fmt.Printf("%p ",fruit) fruit=append(fruit,name) } func main() { fruit:=make(Fruit,0,10) fmt.Printf("%p ",fruit) fruit.Add("apple") fruit=fruit[:1]//重置大小,让数据可见 fmt.Println(fruit) }
分析:切片存在不稳定性,而且经过append之后数据还不可见,因此几乎不用这种形式,具体参考博客的切片篇。
传入切片指针
..
方法调用引发的结构体复制问题
问题提出:无论是值类型或者指针调用值类型方法时,会导致值复制,这个在前面已经详细记录。为什么要提这个问题,主要考虑复制的时候,结构体内部有指针!
在这种情况下,指针也是做为一个“普通值”复制过去了,就是我们常说的浅复制。所以这个问题需要特别注意,有时这个地方也有用处。比如:
/* 指针调用receiver为值的方法,借鸡下蛋,将自己内部的成员赋值 */ type Person struct { age *int //年龄指针 } func (p Person) Set(age int) { fmt.Printf("%p ", &p) //0xc0000cc028 *p.age = age //.比*的优先级高,参考优先级 } func main() { person := &Person{new(int)} //先将指针赋值,避免引发空异常 fmt.Printf("%p ", person) //0xc0000cc018 fmt.Println(person.age == nil) //指针非nil person.Set(20) fmt.Println(*(person.age)) //20 }
这种形式在很多源码中有运行...