[Go语言]类型与接口
Posted jiangwei0512
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Go语言]类型与接口相关的知识,希望对你有一定的参考价值。
类型系统
自定义类型
自定义类型使用关键字type来定义,格式如下:
type newtype oldtype
代码示例:
type INT int
type Map map[string]string
type Person struct {
name string
age int
}
自定义类型的初始化:
package main
import "fmt"
type Person struct {
name string
age int
}
func main() {
a := Person{"Jack", 18} // 不推荐的初始化方式
b := Person{name: "Jack", age: 18} // 推荐
b1 := Person{
name: "Jack",
age: 18, // 因为“}”换行了,这里的“,”是必须的
}
b2 := Person{
age: 18, // 由于指定了成员名,所以初始化时没有顺序要求
name: "Jack",
}
c := new(Person) // 成员都是零值
d := Person{} // 不推荐
d.name = "Jack"
d.age = 18
fmt.Printf("%v\\n", a)
fmt.Printf("%v\\n", b)
fmt.Printf("%v\\n", b1)
fmt.Printf("%v\\n", b2)
fmt.Printf("%v\\n", c)
fmt.Printf("%v\\n", d)
}
上述初始化方式中推荐的是:
b := Person{name: "Jack", age: 18} // 推荐
另外一种是通过类构造函数进行初始化,它不是真正的构造函数,而是普通的函数,但是具有构造作用。
结构体中可以存在匿名成员
,即只给出了成员的类型,但是没有命名。
一个结构体里面不能同时存在某个类型及其指针的匿名成名。
自定义接口类型:
type Reader interface {
Read(p []byte) (n int, err error)
}
接口类型会在接口中介绍。
类型方法
Go语言的类型方式是一种对类型行为的封装。格式如下:
// 类型方法接收者是值类型
func (t TypeName)MethodName(ParamList) (ReturnList) {
// mothod body
}
// 类型方法接收者是指针
func (t *TypeName)MethodName(ParamList) (ReturnList) {
// mothod body
}
这里的接收者指的是类型实例或者类型指针。
类型方式的特点:
- 可以为命名类型增加方法,未命名类型不行;
- 方法的定义必须和类型的定义在同一个包,所以就不能给预声明类型增加方法了;
- 大写开头的方法可以被外部访问,小写的不行;
- 新类型不能调用原有类型的方法,但是底层类型支持的运算可以被新类型继承,另外有一个特例就是struct类型。
示例代码:
package main
import "fmt"
type SliceInt []int
func (s SliceInt) Sum() int {
sum := 0
for _, i := range s {
sum += i
}
return sum
}
func (s *SliceInt) Sum1() int {
sum := 0
for _, i := range *s {
sum += i
}
return sum
}
func main() {
var s SliceInt = []int{1, 2}
fmt.Printf("%d\\n", s.Sum())
fmt.Printf("%d\\n", s.Sum1())
}
在Sum()
的实现中可以直接使用s
这个接收者
,可以认为它作为方法的第一个参数传递给了Sum()
实现函数,因此可以使用,相当于C++类中隐式的this,但是这里没有使用this指针,而是在定义的时候声明在了函数前面,名字也可以自己定义,而且可以是指针或者值。
这里的s.Sum
和s.Sum1
被称为方法值
,它可以赋值给其它函数变量,然后向普通函数一样使用:
f := s.Sum // s.Sum是一个方法值,s.Sum()是方法值调用
fmt.Printf("%d\\n", f()) // f()也是方法值调用
类型方法跟普通函数差别不大,上述的Sum()
方法等价于如下的普通函数(注意这里需要传递类型参数了):
func SliceInt_Sum(s SliceInt) int {
sum := 0
for _, i := range s {
sum += i
}
return sum
}
func main() {
var s SliceInt = []int{1, 2}
fmt.Printf("%d\\n", SliceInt_Sum(s))
}
有一种称为方法表达式
的语法,可以将类型方法调用显式地转换为函数调用,下面的代码将类型方法Sum()
和Sum1()
转换成了普通函数调用的形式:
func main() {
var s SliceInt = []int{1, 2}
fmt.Printf("%d\\n", SliceInt.Sum(s)) // 使用类型本身,而不是它的实例
fmt.Printf("%d\\n", (*SliceInt).Sum1(&s)) // 注意接受者其实是指针,所以参数也需要是指针,所以使用了&
f := SliceInt.Sum // 注意这里不是使用类型实例(即方法值),而是类型本身,所以是方法表达式
fmt.Printf("%d\\n", f(s))
}
转换之后需要注意接收者作为参数传入了,需要注意传值还是传指针。
还需要注意,虽然这里的Sum()
和Sum1()
作为接收者分别使用了值和指针传递,所以方法内部的实现稍有差异,但是调用的时候都是s.X()
。
根据接收者的类型,Go语言中有方法集
一说:
- 接收者为值类型的方法集是S(S指的是接收者为值类型的方法的集合,*S指的是接收者为指针类型的方法的集合);
- 接收者为指针类型的方法集是S和*S;
s.X()
的使用需要保证方法集的对应:
type Data struct{}
func (Data) TestValue() {} // S方法集
func (*Data) TestPointer() {} // *S方法集
func main() {
(*Data)(&struct{}{}).TestPointer() // 指针类型(*Data)的方法集可以是*S,TestPointer属于*S方法集,有对应
(*Data)(&struct{}{}).TestValue() // 指针类型(*Data)的方法集可以是S,TestValue属于S方法集,有对应
(Data)(struct{}{}).TestValue() // 值类型(Data)的方法集是S,TestValue属于S方法集,有对应
// (Data)(struct{}{}).TestPointer() // 值类型(Data)的方法集只有S,TestPointer属于*S方法集,没有对应
}
编译器对调用方法会进行自动转换,即使接收者是指针的方法,仍然可以使用值类型变量进行调用,不过这依赖一定的规则:
- 通过类型字面值量显式地进行方法值调用和方法表达式调用,在这种情况下编译器不会做自动转换,会进行严格地方法集检测:
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
// &struct{}{}是类型字面值
(*Data)(&struct{}{}).TestPointer() // 方法值调用
(*Data)(&struct{}{}).TestValue() // 方法值调用
(Data)(struct{}{}).TestValue() // 方法值调用
Data.TestValue(struct{}{}) // 方法表达式调用
// (Data)(struct{}{}).TestPointer() // 方法值调用,方法集不匹配
// Data.TestPointer(struct{}{}) // 方法表达式调用,方法集不匹配
}
- 通过类型变量进行方法值调用和方法表达式调用,在这种情况下,使用值调用方式调用时会进行自动转换,使用表达式调用方式调用时编译器不会进行转换,会进行严格地方法集检查:
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
// 定义类型变量
var a Data = struct{}{}
// 方法表达式调用编译器不会自定转换
Data.TestValue(a)
// Data.TestValue(&a) // 错误,因为不会自动转换
(*Data).TestPointer(&a)
// Data.TestPointer(&a) // 错误,因为不会自动转换
// 方法值调用编译器会进行自动转换
a.TestValue()
(&a).TestValue() // 编译器会转换成a.TestValue()
a.TestPointer() // 编译器会转换成(&a).TestPointer()
(&a).TestPointer()
}
组合和方法集
前面提到使用type
定义新类型不会继承原有类型的方法,但是命名结构类型是一个特例:命名结构类型可以嵌套其它的命名结构类型,外层的结构类型可以调用内部成员类型的数据和方法。
package main
import "fmt"
type X struct {
a int
}
type Y struct {
X // 匿名成员
b int
}
type Z struct {
Y
c int
}
func main() {
x := X{a: 1}
y := Y{X: x, b: 2}
z := Z{Y: y, c: 3}
fmt.Printf("%d\\n", z.a) // 1
}
这里的z.a
相当于z.Y.X.a
,而X
和Y
的关系,Y
和Z
的关系,就是组合
,虽然有点像继承,但是组合
用来表示这种形式更合适。
需要注意这里Y
类型中的X
,Z
类型中的Y
都必须是匿名的,否则没有用。y
和z
的实例化中,都直接用了X
、Y
来赋初始值,这是访问匿名成员的方式。
另外,也存在同名数据的情况,这个时候就需要全路径了,而不是直接z.a
,下面是一个例子:
package main
import "fmt"
type X struct {
a int
}
type Y struct {
X // 匿名成员
a int
}
type Z struct {
Y
a int
}
func main() {
x := X{a: 1}
y := Y{X: x, a: 2}
z := Z{Y: y, a: 3}
fmt.Printf("%d\\n", z.a)
fmt.Printf("%d\\n", z.Y.a)
fmt.Printf("%d\\n", z.Y.X.a)
}
不过在实际的使用当中,最好就不要出现同名的情况。
方法也类似,可以简写,也可以有同名,此时按照从外到内的方式找到同名的方法并调用(当然,只会调用最外层的那个同名方法,而不是每个都调用一遍)。
package main
import "fmt"
type X struct {
a int
}
func (x X) Print() {
fmt.Printf("X: %d\\n", x.a)
}
type Y struct {
X // 匿名字段
a int
}
func (y Y) Print() {
fmt.Printf("Y: %d\\n", y.a)
}
type Z struct {
Y
a int
}
func (z Z) Print() {
fmt.Printf("Z: %d\\n", z.a)
}
func main() {
x := X{a: 1}
y := Y{X: x, a: 2}
z := Z{Y: y, a: 3}
z.Print() // Z: 3
z.Y.Print() // Y: 2
z.Y.X.Print() // X: 1
}
有点类似于子类覆盖父类的方法。
组合也有方法集的规则:
- 若类型S包含匿名成员T,则S的方法集包含T的方法集;
- 若类型S包含匿名成员*T,则S的方法集包含T和*T的方法集;
- 不管类型S中嵌入的匿名成员是T还是*T,*S方法集总是包含T和*T的方法集;
接口
接口是一组方法签名的集合。接口没有具体的实现逻辑,也不能定义数据成员。
一个具体类型实现接口不需要在语法上显式地声明,只要具体类型的方法是接口方法集的超集(即具体类型的方法集包含了接口方法集中的所有方法),就代表该类型实现了接口。
最常用的接口字面量类型是空接口interface{}
,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或者传递给空接口,包括非命名类型的实例。
Go语言的接口有两种,接口字面值类型:
interface {
Method1
Method2
// ...
}
命名类型:
type InterfaceName interface {
Method1
Method2
// ...
}
注意这里的Method1
、Method2
是方法声明(= 方法名 + 方法签名)。
接口实现还支持嵌套匿名接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReaderWriter interface {
Reader // 匿名接口
Writer // 匿名接口
}
ReaderWriter
接口跟下面的是一样的:
type ReaderWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
声明新接口类型的特点:
- 接口的命名一般以“er”结尾;
- 接口定义的内部方法声明不需要使用
func
关键字; - 在接口定义中,只有方法声明而没有方法实现,方法声明如下:
MethodName(param-list) (return-list)
接口绑定具体类型的实例的过程称为接口初始化
。接口的初始化有两种方式:
- 实例赋值接口;
- 接口变量a赋值给接口变量b,这要求b的方法集是a的方法集的子集,即b有的方法a也必须要有;
package main
import "fmt"
type Printer interface {
Print()
}
type S struct{}
func (s S) Print() {
fmt.Printf("Print\\n")
}
func main() {
var i Printer
fmt.Printf("%T\\n", i) // i是nil
// i.Print() // 因为是nil,所以不能直接调用Print()
i = S{} // i有一个Print方法,而S也实现相同的Print方法,所以可以对接口进行初始化
fmt.Printf("%T\\n", i)
i.Print()
}
上例中i
是一个接口,i = S{}
就是实例赋值接口来初始化接口,之后才能够调用接口。
需要注意Printer
接口中的方法Print()
和S
结构体中的Print()
不仅签名需要一样,方法名也需要一样,否则会报错:
func (s S) Print111() {
fmt.Printf("Print\\n")
}
这里的方法变成了Print111()
就会报错:
# command-line-arguments
.\\interface2.go:20:4: cannot use S{} (type S) as type Printer in assignment:
S does not implement Printer (missing Print method)
同样,签名不一样也不行:
func (s S) Print(i int) {
fmt.Printf("Print\\n")
}
这个例子中会报错:
# command-line-arguments
.\\interface2.go:20:4: cannot use S{} (type S) as type Printer in assignment:
S does not implement Printer (wrong type for Print method)
have Print(int)
want Print()
接口绑定的具体实例的类型被称为接口的动态类型
;接口被定义时,其类型已经确定下来,这个被称为接口的静态类型
。
package main
import "fmt"
type Printer interface {
Print()
}
type S struct{}
func (s S) Print() {
fmt.Printf("SSSS Print\\n")
}
type P struct{}
func (p P) Print() {
fmt.Printf("PPPP Print\\n")
}
func main() {
var i Printer
fmt.Printf("%v\\n", i) // i是nil
// i.Print() // 因为是nil,所以不能直接调用Print()
i = S{}
fmt.Printf("%T\\n", i)
i.Print()
i = P{}
fmt.Printf("%T\\n", i)
i.Print()
}
这里的S
和P
就是两个动态类型,程序执行的结果:
<nil>
main.S
SSSS Print
main.P
PPPP Print
接口类型是“第一公民“,可以用在任何使用变量的地方,比如:
- 作为结构体成员;
- 作为函数或方法的形参;
- 作为函数或方法的返回值;
- 作为其它接口定义的成员。
类型断言
类型断言的语法:
i.(TypeName)
i
是接口变量,而不是具体类型变量。TypeName
可以是接口类型名,也可以是具体类型名:
- 如果是具体类型名,则类型断言用于判断变量
i
绑定的实例类型是否就是具体类型TypeName
; - 如果是接口类型名,则类型断言用于判断变量
i
绑定的实例类型是否同时实现了TypeName
接口。
类型断言的使用方式一:
o := i.(TypeName)
如果TypeName
是具体类型名,此时如果接口i
绑定的实例类型是就是具体类型TypeName
,则变量o
的类型就是TypeName
,变量o
的值就是接口绑定的实例值的副本;如果TypeName
是接口类型名,如果接口i
绑定的实例类型满足接口类型TypeName
,则变量o
的类型就是接口类型TypeName
,o
底层绑定的具体类型实例是i
绑定的实例的副本;如果上述两种情况都不满足,则程序抛出panic。
类型断言的使用方式二:
if o, ok := i.(TypeName); ok {
// 具体代码
}
如果TypeName
是具体类型名,此时如果接口i
绑定的实例类型就是具体类型TypeName
,则ok
为true
,变量o
的类型就是TypeName
,变量o
的值就是接口绑定的实例值的副本;如果TypeName
是接口类型名,如果接口i
绑定的实例类型满足接口类型TypeName
,则ok
为true
,变量o
的类型就是接口类型TypeName
,o
底层绑定的具体类型实例是i
绑定的实例的副本;如果上述两种情况都不满足,则ok
为false
,变量o
是TypeName
类型的零值。
package main
import "fmt"
type Inter interface {
Ping()
Pong()
}
type Anter interface {
Inter
String()
}
type St struct {
name string
}
func (St) Ping() {
fmt.Printf("Ping\\n")
}
func (*St) Pong() {
fmt.Printf("Pong\\n")
}
以上是关于[Go语言]类型与接口的主要内容,如果未能解决你的问题,请参考以下文章
go语言学习笔记 — 进阶 — 接口(3.1):接口与类型的关系 —— 一个类型可以实现多个接口
go语言学习笔记 — 进阶 — 接口(3.2):接口与类型的关系 —— 多个类型可以实现一个(相同)接口