方法和接口进阶(Go语言)

方法

定义

方法是与对象实例绑定的特殊函数。

方法是面向对象编程的基础概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例都有各自不同的独立特征,以属性和方法来暴露对外通行接口。普通函数则是专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。

方法是有关联状态的,函数是没有的。

方法和函数语法区别在于前者有前置实例接收参数(receiver),编译器以便确定方法所属类型。在某些语言里,尽管没有显式定义,但会在调用时隐式传递this实例参数。

可以为当前包,以及除接口和指针以外的任何类型定义方法。

func (receiver 类型)函数名(参数列表)(返回值列表){}

方法同样不支持重载(overload),receiver参数名没有限制,按惯例选用简短有意义的名称。

type N int

func (N) test(){
	println("zz")
}

方法可看作特殊的函数,那么receiver的类型自然可以是基础类型或指针类型。

可使用实例值或指针调用方法,编译器会根据方法receiver类型自动在基础类型和指针类型间转换。

不能使用多级指针调用。

指针类型的receiver必须时合法指针(包括nil),或能获取实例地址。

将方法看作普通函数,就能很容易理解receiver的传参方式。

receiver类型选择:

  • 要修改实例的状态,用*T
  • 无须修改状态的小对象或固定值,建议用T
  • 大对象建议用*T,以减少复制成本
  • 引用类型,字符串,函数等指针包装对象,用T
  • 若包含Mutex等同步字段,用*T,避免因复制造成锁操作无效
  • 其他无法确定的情况,都用*T

匿名字段

可以像访问匿名字段成员那样调用其方法,由编译器负责查找。

type data struct {
	sync.Mutex
	buf [1024]byte
}

func main() {
	d:=data{}
	d.Lock()   //编译会处理为sync.(Mutex).Lock()
	defer d.Unlock()
}

方法也存在同名遮蔽。但是可以利用这种实现类似覆盖(override)操作。

type user struct {

}
type manager struct {
	user
}

func (user) toString() string {
	return "user"
}
func (m manager) toString() string {
	return m.user.toString()+"; manager"
}
func main() {
	var m manager

	println(m.toString())
	println(m.user.toString())
}

尽可能直接访问匿名字段的成员和方法,但它们依然不属于继承关系。

方法集

类型有一个与之相关的方法集(method set),这决定它是否实现了某个接口。

  • 类型T方法集包含所有receiver T方法。
  • 类型*T方法集包含所有receiver T + *T方法
  • 匿名嵌入式S,T方法集包含所有receiver S 方法
  • 匿名嵌入式*S,T方法集包含所有receiver S + *S方法
  • 匿名嵌入式S或*S, * T方法集包含所有receiver S 方法

可利用反射(reflect)测试这些规则。

方法集仅影响接口实现和方法表达式转换,与通过实例或实例指针调用方法无关。实例并不适用方法集,而是直接调用。

匿名字段就是为方法集准备的。否则,完全没必要为少写个字段名儿大费周章。

面向对象的三大特征“封装”,“继承”,“多态”,Go语言仅实现了部分特征,更倾向于“组合优于继承”。这种思想:将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一块,共同实现对外接口。其简短一致的调用方式,更隐藏了内部的实现细节。

组合没有父子依赖,不会破坏封装。且整体和局部松耦合,可任意增加来实现扩展。各单元持有单一指责,互无关联,实现和维护更加简单。

表达式

方法和函数一样,除直接调用外,还可赋值给变量,或作为参数传递。依着具体引用方法的不同,可分别expression和value两种状态。

Method Expression

通过类型引用的Method expression 会被还原为普通函数样式,receiver是第一参数,调用时许显式传参。至于类型,可以式T或*T,只要目标村在该类型方法集中即可。

type N int

func (n N) test()  {
	fmt.Printf("test.n: %p, %d\n",&n,n)
}
func main() {
	var n N =110

	fmt.Printf("main.n %p, %d\n",&n ,n)

	f1:= N.test
	f1(n)
	f2:=(*N).test
	f2(&n)
}

Method Value

基于实例或指针引用的Method Value,参数签名不会改变,依旧按正常方式调用。

但当method Value 被赋值给变量或作为参数传递时,会立即计算并赋值该方法执行锁需要的receiver对象,与其绑定,以便在稍后执行时,能隐式传入receiver参数。

type N int

func (n N) test()  {
	fmt.Printf("test.n: %p, %d\n",&n,n)
}
func main() {
	var n N =110
	p :=&n
	n++

	f1:=n.test

	n++
	f2:=n.test

	n++
	fmt.Printf("mian.n: %p, %d\n",p,n)

	f1()
	f2()
}

当method value 作为参数时,会复制receiver在内的整个method value。

如果目标方法的recover是指针类型,被复制仅是指针。

只要receiver参数类型正确,使用nil同样可以执行。

type N int

func (N) value() {

}
func (*N) pointer() {

}
func main() {
	var p *N

	p.pointer()       // method value
	(*N)(nil).value() //method value
	(*N).pointer(nil) //method expression
}

接口

定义

接口代表一种调用契约,是多个方法声明的集合。

在某些动态语言里,接口(interface)也被称作协议(protocol)。准备交互的双方,共同遵守事先约定的规则,使得在无须知道对方身份的情况下进行写作。接口要实现的是做什么,而不关心怎样做,谁来做。

接口解除了类型依赖,有助于减少用户可视方法,屏蔽内部结构和实现细节。但不能滥用接口。**接口实现机制会有运行期开销。**对于相同包,或者不会频繁变化的内部模块之间,并不需要抽象出接口来强行分离。接口最常见的使用场景,是对包外提供访问,或预留扩展空间。

**Go语言实现机制很简洁,只要目标类型方法集内包含接口声明的全部方法,就被视为实现了该接口,无须做显示声明。**目标类型可实现多个接口。

从内部实现来看,接口自身也是一种结构类型,只是编译器会对其做出很多限制。

type iface struct {
	tab *itab
	data unsafe.Pointer
}
  • 不能有字段。
  • 不能定义自己的方法。
  • 只能声明方法,不能实现。
  • 可嵌入其他接口类型。

接口通常以er作为名称后缀,方法名是声明组成部分,但参数名可不同或省略。

type tester interface {
	test()
	string() string
}

type data struct {

}

func (*data) test()  {

}
func (data) string()string  {
	return "zz"
}

func main() {
	var d data

	var t tester = &d
	t.test()
	println(t.string())
}

编译器根据方法集来判断是否实现了接口,显然在上例只有*data才复合 tester的要求。

如果接口没有任何方法声明,那么就是一个空接口(interface{}),它的用途类似面向对象里的根类型Object,可被赋值为任何类型的对象。

接口变量默认值是nil,如果实现接口的类型支持,可做相等运算。

可以像匿名字段那样,嵌入其他接口。目标类型方法集中必须拥有包含嵌入接口方法在内的全部方法才算实现了该接口。

嵌入其他接口类型,相当于将其声明的方法集导入。这就要求不能有同名方法,因为不支持重载。不能嵌入自身或循环嵌入,会导致递归错误。

type stringer interface {
	string() string
}
type tester interface {
	stringer
	test()
}

type data struct {

}
func (*data) test(){
	fmt.Printf("zzz")
}
func (data) string() string {
	return ""
}
func main() {
	var d data

	var t tester =&d
	t.test()
	println(t.string())
}

超集接口变量可隐式转换为子集,反过来不行。

支持匿名接口类型,可直接用于变量定义,或作为结构字段类型。

执行机制

接口使用一个名itag的结构存储运行期所需要的相关类型信息。

type iface struct{
	tab *itab   // 类型信息
	data unsafe.Pointer // 实际对象指针
}

type itab struct{
	inter *interfacetype  //接口类型
	_type *_type          //实际对象类型
	fun  [1]uintptr       //实际对象方法地址
}

相关类型信息里保存了接口和实际对象的元数据。itab还用fun数组(布丁长结构)保存了实际方法地址,从而实现在运行期对目标方法的动态调用

接口将对象赋值给接口变量是,会复制该对象。

只有当接口变量内部的两个指针(itab,data)都为nil时,接口才等于nil.

参考资料
《Go语言学习笔记》 雨痕

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值