Go 基本概念速记速查之结构体、方法、接口

date
May 31, 2023
slug
golang-start-struct-method-interface
status
Published
tags
Go
summary
Go 基本概念汇总速记
type
Post

结构体

定义

结构体声明变量,通过 struct_name {x,y,z} 逐一赋值,或者指定每一个 k v 来赋值
结构体访问,通过 struct_name.field_name 访问
 

结构体指针

如果一个指针 p 指向一个结构体 Vertex,理论上访问 Vertex 的属性 X,需要用 (*p).X,但 go 也支持简便的隐式间接引用,直接用 p.X
 

结构体的方法

理论上来说不光能给结构体添加方法,实际上可以给任何类型添加方法,而结构体是通过 type struct 定义出来的一种类型,所以能够为其添加方法也就很合理了。
在 func 关键字后跟着一个类型声明,称之为 receiver,表示该方法是某类型特有方法。
方法调用时相当于将当前类型的实例传给方法前面的 receiver,具体来说下面例子中,就是将 mt 传给了方法定义中的 mt receiver。
默认调用方法都是通过值传递的方式,也就意味着在方法内修改变量内容不会对外产生影响,想改变内部的值需要传递引用, 即 method2
 

结构体通过组合实现“继承”效果

go 本身不支持继承,不过可以使用组合模式来实现继承的效果。
通过结构体匿名嵌入的方式实现继承,Dog 中嵌入一个匿名的 Animal 的结构体,实现 Dog 继承 Animal 的属性和方法。
通过 dog 调用时可以省略中间的匿名结构体
Animal.go
Dog.go
通过在 Dog 的属性中嵌入一个 Animal 匿名结构体,实现继承关系
使用时,初始化一个 Dog 实例,初始化时需要传入一个 Animal 实例,通过 dog 实例调用 Animal 的方法时可以省略中间匿名结构体
嵌入继承的不完备性,即多态无法体现
在 main.go 中增加一个调用:
这说明 Dog 中的 Sound 方法没有被调用。
使用 interface 来实现继承
使用 interface 定义一组方法,任何结构体只要实现了 interface 中所有方法,就被视为实现了这个接口。
然后再单独建立一个函数,专门接收该接口类型为参数,这样就可以处理各种实现了该接口的类型,实现多态。
实际上这里的问题是,go 没有传统概念中的继承以及继承之后的方法重写,想要通过方法的重写实现多态,只能通过 interface 来声明不同类型拥有相同签名的方法,从而达到多态效果。
 

给类型附加方法

go 没有类,可以通过在 type 类型上定义方法的方式来实现类的效果,一般来说,
  1. 先是通过 struct 定义一个结构体
  1. 再声明这个结构体为一种 type 类型
  1. 然后定义函数,函数定义中指定一个 receiver 接受者为该类型
  1. 如此一来就定义了一个结构体类型,且该类型具有某个方法
注意 go 支持的是在 type 上定义 method,而不是在 struct 上定义。
方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
方法 method 仅仅就是一个加上了接受者 receiver 参数的函数,下面这个例子中,同样是 Abs 函数,换成了用函数方式调用
注意,定义 method 并不是只能在 struct 上,而是 type上,只要是 type 就可以,比如 Abs 方法定义在类型 MyFloat 上,f 方法定义在类型 fdfdf 上
 

方法接收者 receiver

值接收者 value receiver 和指针接收者 pointer receiver
如何理解接受者?定义一个函数,这个函数是给谁定义的?这就是接受者的意思。
接收者分两种
  • 值接收者 value receiver
  • 指针接收者 pointor receiver:在定义接收者时,在类型前面加 *,使 v 变成一个指针
如果去掉了 *,变成值接收者,那么修改的 Vertex 将是一个副本,就像普通函数参数那样
 
编译器自动处理
如果一个函数接收一个指针类型,那么调用时必须通过指针调用
而方法中,如果定义的是指针接收者,那么调用时,既可以使用值调用,也可以使用指针调用:
也就是说,如果一个方法的接收者是个指针,即方法定义好之后是给指针使用的,那么使用时既可以通过指针调用,也可以通过普通值调用,因为在背后,编译器自动将值调用 v.Scale(5) 编译成了 (&v).Scale(5) 。
反之也成立:
p.Abs() 被解释成了 (*p).Abs()
使用指针接收者的原因有二:
  • 首先,方法能够修改其接收者指向的值。
  • 其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。
一般来说对于一个类型,不应该混用值和指针接收者,而是二选一。
 

interface 接口

接口是一组方法的集合,接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。过程中不需要显式使用 implement 实现什么,这叫做隐式声明。
 
接口在引用值类型和指针类型时的区别:
下面代码中,接口变量 a 赋值为 f 和 &v 都可以,因为 MyFloat 和 *Vertex 都实现了 Abs 方法,也就隐式实现了 Abser 接口,而 Vertex 并没有实现 Abs 方法,也就没有实现 Abser 接口。
 

接口值 interface value

接口值可以被看做是一个由 value 和一个具体类型组成的元组,一个接口值持有一个特定底层具体类型的值。
调用接口值时,就是执行元组中某个类型的那个方法。
这句话说的应该就是面向接口编程的执行过程,一个 Interface i = 实现了该接口的类型的实例,
调用时 i.xxxFunc() ,其实是调用的该实例的 xxxFunc()
下面代码中, var i I ,定义 i 是一个接口 I 的值,i 可以指向所有实现了接口 I 的类型。
i 可以看成是 (&T{"Hello"}, *T) 这样一个元组
 
下面描述的是接口值 i 直接指向一个空的 *T,也就是 i 的元组中,值那个部分是 nil,不过这样也是可以编译的。
 

空接口 interface{}

空接口用来处理未知类型的值,或叫任意类型的值
interface{}
空接口可以指代任何类型,因为任何类型都实现了 0 个方法。
空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。
 

类型断言 Type assertions

提供了访问接口值底层具体值的方式。
If i does not hold a T the statement will trigger a panic.
If i holds a T, then t will be the underlying value and ok will be true.
If not, ok will be false and t will be the zero value of type T, and no panic occurs.
 

类型选择 Type switches 实现泛型

可实现泛型的效果,语法类似 switch 和 type assertion 的结合,把 type assertion 中的 i.(T) 换成了 i.(type) ,然后得到的结果跟 case 比较
 

两个常见接口

Stringer 接口

Stringer 接口是 fmt 包中最常见的接口,实现之后用来通过字符串描述自身,也就是在 fmt.Print 打印时调用的
 

错误 Error

Go 程序使用 error 值来表示错误状态。与 fmt.Stringer 类似,error 类型是一个内建接口:
在调用函数后,验证 err 是否是 nil,来判断是否出错
 

© 菜皮 2020 - 2023