北京网站建设的服务公司,自己搭建视频播放网站,深圳龙华区龙华街道高坳新村,网站的建设与管理自考提到面向对象编程中的继承#xff0c;许多人脑海中可能会浮现出 Java、C 等语言中那一套熟悉的类继承体系。然而#xff0c;Go 语言作为一门别具一格的编程语言#xff0c;并没有遵循传统的继承模式。那么#xff0c;在 Go 语言的世界里#xff0c;它是怎样实现类似于继承…提到面向对象编程中的继承许多人脑海中可能会浮现出 Java、C 等语言中那一套熟悉的类继承体系。然而Go 语言作为一门别具一格的编程语言并没有遵循传统的继承模式。那么在 Go 语言的世界里它是怎样实现类似于继承的功能让代码变得更加高效和灵活的呢这就不得不深入探讨 Go 语言中的 struct 和 interface 了。接下来就让我们一同开启这段探索之旅。
结构体
结构体(Struct)是一种聚合类型里面可以包含任意类型的值这些值就是我们定义的结构体的成员也称为字段。在 Go语言中要自定义一个结构体需要使用 typestruct 关键字组合。
type person struct {name stringage int
}// 通用格式
type structName struct {fieldName typeName
}在定义结构体时字段的声明方法与平时声明一个变量是一样的都是变量名在前类型在后只不过在结构体中变量名称为成员名或字段名。
结构体的成员字段并不是必需的也可以一个字段都没有这种结构体称为空结构体。空结构体在 Go语言中是一个比较神奇且全能的存在我们在实际开发时经常会用到这个东西后面会专门做内容讲解空结构体的相关点。 结构体也是一种类型所以对于以后自定义的结构体我会称为某结构体或某类型两者是一个意思。比如person结构体和person类型其实是一个意思。 定义好结构体后就可以使用它了因为它是一个聚合类型所以可以比普通的类型携带更多数据。 声明和使用
结构体类型也可以使用与普通的字符串、整型一样的方式进行声明和初始化。
// 完整声明
// 声明后未初始化时默认会使用结构体里字段的零值。
var p person// 简短声明
p : person{随便寻个地方, 22}采用字面量初始化结构体时初始化值的顺序很重要必须与字段定义的顺序一致。那么是否可以不按照顺序初始化呢当然可以只不过需要指出字段名称。
p : person{age:22, name:随便寻个地方}当然你也可以只初始化字段age字段name使用默认的零值如下面的代码所示仍然可以编译通过。
p : person{age:22}声明了一个结构体变量后就可以使用它了。在Go语言中访问一个结构体的字段与调用一个类型的方法一样都是使用点操作符 “.”。
fmt.Println(p.name, p.age)结构体中的字段
结构体的字段可以是任意类型包括自定义的结构体类型比如下面的代码
type person struct {name stringage intaddr address
}type address struct {province stringcity string
}通过这种方式用代码描述现实中的实体会更匹配复用程度也更高。对于嵌套结构体字段的结构体其初始化与正常的结构体大同小异只需要根据字段对应的类型初始化即可。
p:person{age:30,name:飞雪无情,addr:address{province: 北京,city: 北京,},
}如果需要访问结构体最里层的 province 字段的值同样也可以使用点操作符只不过需要使用两个点。
// 第一个点获取 addr第二个点获取 addr 的 province。
fmt.Println(p.addr.province)接口
接口是和调用方的一种约定它是一个高度抽象的类型不用和具体的实现细节绑定在一起。接口要做的是定义好约定告诉调用方自己可以做什么但不用知道它的内部实现这和我们见到的具体的类型如 int、map、slice 等不一样。
接口的定义和结构体稍微有些差别虽然都以 type关键字开始但接口的关键字是 interface表示自定义的类型是一个接口。也就是说 Stringer 是一个接口它有一个方法 String() string。
type Stringer interface {String() string
}针对 Stringer 接口来说它会告诉调用者可以通过它的 String() 方法获取一个字符串这就是接口的约定。至于这个字符串怎么获得的长什么样接口不关心调用者也不用关心因为这些是由接口实现者来做的。
接口的实现
接口的实现者必须是一个具体的类型继续以 person 结构体为例让它来实现 Stringer 接口。
func (p person) String() string{return fmt.Sprintf(the name is %s,age is %d,p.name,p.age)
}给结构体类型 person 定义一个方法这个方法和接口里方法的签名名称、参数和返回值一样这样结构体 person 就实现了 Stringer 接口。 注意如果一个接口有多个方法那么需要实现接口的每个方法才算是实现了这个接口。 实现了 Stringer 接口后就可以使用了。
func printString(s fmt.Stringer){fmt.Println(s.String())
}这个被定义的函数 printString它接收一个 Stringer 接口类型的参数然后打印出 Stringer 接口的 String 方法返回的字符串。
printString 这个函数的优势就在于它是面向接口编程的只要一个类型实现了 Stringer 接口都可以打印出对应的字符串而不用管具体的类型实现。
因为 person 实现了 Stringer 接口所以变量 p 可以作为函数 printString 的参数可以用如下方式打印
printString(p)结果为
the name is 随便寻个地方,age is 22现在让结构体 address 也实现 Stringer 接口如下面的代码所示
func (addr address) String() string{return fmt.Sprintf(the addr is %s%s,addr.province,addr.city)
}因为结构体 address 也实现了 Stringer 接口所以 printString 函数不用做任何改变可以直接被使用打印出地址。
printString(p.addr)
//输出the addr is 北京北京这就是面向接口的好处只要定义和调用双方满足约定就可以使用而不用管具体实现。接口的实现者也可以更好的升级重构而不会有任何影响因为接口约定没有变。
值接收者和指针接收者
我们已经知道如果要实现一个接口必须实现这个接口提供的所有方法而且在上一章讲解方法的时候我们也知道定义一个方法有值类型接收者和指针类型接收者两种。二者都可以调用方法因为Go语言编译器自动做了转换所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中值类型接收者和指针类型接收者不一样下面我会详细分析二者的区别。
在上一小节中已经验证了结构体类型实现了Stringer接口那么结构体对应的指针是否也实现了该接口呢我通过下面这个代码进行测试
printString(p)测试后会发现把变量 p 的指针作为实参传给 printString 函数也是可以的编译运行都正常。这就证明了以值类型接收者实现接口的时候不管是类型本身还是该类型的指针类型都实现了该接口。
示例中值接收者(p person)实现了 Stringer 接口那么类型 person 和它的指针类型 *person 就都实现了 Stringer 接口。
现在我把接收者改成指针类型如下代码所示
func (p *person) String() string{return fmt.Sprintf(the name is %s,age is %d,p.name,p.age)
}修改成指针类型接收者后会发现示例中这行printString§代码编译不通过提示如下错误
./main.go:17:13: cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)意思就是类型 person 没有实现 Stringer 接口。这就证明了以指针类型接收者实现接口的时候只有对应的指针类型才被认为实现了该接口。
我用如下表格为你总结这两种接收者类型的接口实现规则
方法接收者实现接口的类型(p person)person 和 *person(p *person)*person
当值类型作为接收者时person 类型和 *person 类型都实现了该接口。当指针类型作为接收者时只有 *person 类型实现了该接口。
可以发现实现接口的类型都有 *person 这也表明指针类型比较万能不管哪一种接收者它都能实现该接口。
继承和组合
在 Go语言中没有继承的概念所以结构体、接口之间也没有父子关系Go语言提倡的是组合利用组合达到代码复用的目的这也更灵活。
我们以 Go 语言 io 标准包自带的接口为例讲解类型的组合也可以称之为嵌套。
type Reader interface {Read(p []byte) (n int, err error)
}type Writer interface {Write(p []byte) (n int, err error)
}//ReadWriter是Reader和Writer的组合
type ReadWriter interface {ReaderWriter
}ReadWriter 接口就是 Reader 和 Writer 的组合组合后ReadWriter 接口具有 Reader 和 Writer 中的所有方法这样新接口 ReadWriter 就不用定义自己的方法了组合 Reader 和 Writer 的就可以了。
不止接口可以组合结构体也可以组合现在把 address 结构体组合到结构体 person 中而不是当成一个字段。
type person struct {name stringage uintaddress
}直接把结构体类型放进来就是组合不需要字段名。组合后被组合的 address 称为内部类型person 称为外部类型。修改了 person 结构体后声明和使用也需要一起修改。
p:person{age:30,name:飞雪无情,address:address{province: 北京,city: 北京,},}
//像使用自己的字段一样直接使用
fmt.Println(p.province)因为 person 组合了 address所以 address 的字段就像 person 自己的一样可以直接使用。
类型组合后外部类型不仅可以使用内部类型的字段也可以使用内部类型的方法就像使用自己的方法一样。如果外部类型定义了和内部类型同样的方法那么外部类型的会覆盖内部类型这就是方法的覆写。关于方法的覆写这里不再进行举例你可以自己试一下。 小提示方法覆写不会影响内部类型的方法实现。 类型断言
有了接口和实现接口的类型就会有类型断言。类型断言用来判断一个接口的值是否是实现该接口的某个具体类型。
还是以我们上面小节的示例演示我们先来回忆一下它们如下所示
func (p *person) String() string{return fmt.Sprintf(the name is %s,age is %d,p.name,p.age)
}func (addr address) String() string{return fmt.Sprintf(the addr is %s%s,addr.province,addr.city)
}可以看到*person 和 address 都实现了接口 Stringer然后我通过下面的示例讲解类型断言
var s fmt.Stringer
s p1
p2 : s.(*person)
fmt.Println(p2)如上所示接口变量 s 称为接口 fmt.Stringer 的值它被 p1 赋值。然后使用类型断言表达式 s.(*person)尝试返回一个 p2。如果接口的值 s 是一个 *person那么类型断言正确可以正常返回 p2。如果接口的值 s 不是一个 *person那么在运行时就会抛出异常程序终止运行。 小提示这里返回的 p2 已经是 *person 类型了也就是在类型断言的时候同时完成了类型转换。 在上面的示例中因为 s 的确是一个 *person所以不会异常可以正常返回 p2。但是如果我再添加如下代码对 s 进行 address 类型断言就会出现一些问题
a:s.(address)
fmt.Println(a)这个代码在编译的时候不会有问题因为 address 实现了接口 Stringer但是在运行的时候会抛出如下异常信息
panic: interface conversion: fmt.Stringer is *main.person, not main.address这显然不符合我们的初衷我们本来想判断一个接口的值是否是某个具体类型但不能因为判断失败就导致程序异常。考虑到这点Go 语言为我们提供了类型断言的多值返回如下所示
a,ok:s.(address)
if ok {fmt.Println(a)
}else {fmt.Println(s不是一个address)
}类型断言返回的第二个值 “ok” 就是断言是否成功的标志如果为 true 则成功否则失败。
总结
这节课虽然只讲了结构体和接口但是所涉及的知识点很多并且非常杂乱需要深入地学习。且由于涉及到面向对象相关的内容在面试的时候很有可能会被问到一些比较复杂的问题这些在后面都会一一讲解。
结构体是对现实世界的描述接口是对某一类行为的规范和抽象。通过它们我们可以实现代码的抽象和复用同时可以面向接口编程把具体实现细节隐藏起来让写出来的代码更灵活适应能力也更强。