写在前面:
Go语言中没有 类
的概念,所以也没有封装、继承、多态(面向对象的三大特性)的概念;
但是,通过结构体的内嵌再配合接口,可以变相实现,同时比面向对象具有更高的扩展性和灵活性。接口 没有强制命名规则,但习惯以 er 结尾
。
下面用一个简单样例展示一下如何实现:
// 定义一个名为Cat的struct,约等于其他语言的 Cat 类(封装伪实现)
type Cat struct {
// 包含 名称和颜色 两个初始化变量
Name string
Color string
}
// 构造函数(不强制,一般用New开头):通过Color初始化Cat类
func NewCatByColor(c string) *Cat {
// 如果结构体比较复杂,值拷贝de性能开销会比较大,所以一般返回的是结构体的指针
return &Cat{
Color: c,
}
}
// 构造函数2(约等于重载):通过Name初始化Cat类(多态伪实现)
func NewCatByName(n string) *Cat {
return &Cat{
Name: n,
}
}
// 如上的示例相当于只简单定义了一个Cat类并初始化了其内置变量;
// 如果我们要输出猫咪的叫声,这时需要给类添加一个方法:
func (self *Cat) Meow() {
self.Name = "Iam a Cat:" + self.Name
fmt.Println(*self, "喵喵叫")
}
func main() {
// 实例化"类" & 输出结果
cat1 := NewCatByColor("Blue")
cat1.Name = "Jazz" // 约等于调用类的public变量
cat1.Meow(3) // 约等于调用类方法
cat2 := NewCatByName("MJ")
cat2.Color = "Black"
fmt.Println(cat2)
}
上面示例中的 Meow,在go语言中称作 方法(Method),是一种作用于特定类型变量的函数。
这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的 this 或者 self,比如上面我们就取了 self 这个变量名,但不建议这这么写,下文有解释;
这里只是为了便于理解,所以取名 this;
go方法(Method)的定义格式如下:
func (接收者变量名 接收者类型) 方法名(参数列表) (返回参数) {
// 函数主体逻辑
}
1.接收者变量名:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母(例如,Cat 类型的接收者变量应该命名为 c),而不是self、this之类的命名,如果这么命名,会提示:receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"
;
2.接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。如果想同时在方法中修改接受者(类中变量)的值或者type比较复杂内存开销大,建议指针;
3.方法名、参数列表、返回参数:具体格式与函数定义相同。
结构体字段的可见性
结构体中字段大写开头表示可公开访问(public),小写表示私有(private),仅在定义当前结构体的包中可访问。
其实很好理解,传统的OOP语言要支持继承,所以有protected
这个权限概念,既然不支持OOP也就不需要它了。
如何实现 继承?
上面的Demo基本实现了一个"类"的创建与实例化和调用,但OOP最重要的特性之一 继承
又该怎么实现呢?
还是用上面的示例,我们追加一个Animal
类,毕竟白猫黑猫都属于 Animal;
// Animal 基类包含 名称和颜色 两个初始化变量
type Animal struct {
// 和其他语言相同:相同类型的变量可在一行同时定义
Name, Color string
}
// 创建Cat伪子类,将Animal内嵌到Cat,以实现伪继承
type Cat struct {
*Animal // 匿名字段,即字段名缺省,默认用该类型名作为字段名
Age int
Name string // 和Animal重名的字段会被当前的Cat重写
}
// 伪继承模式的构造函数实现:3种模式,任选其一
func NewCatByName(n string) *Cat {
return &Cat{
// 匿名字段使用该类型名作字段名即可
Animal: &Animal{
Name: n,
},
Age: 1,
}
// // 不带字段名的初始化需给所有字段赋初值,且顺序一致
// return &Cat{
// &Animal{
// Name: n,
// },
// 1,
// "NameCat",
// }
// // 或者:先实例化再修改值
// c := &Cat{} // 实例化Cat,同时,其伪父类也被实例化
// c.Name = n // 填充构造函数的初始值
// return c
}
func main() {
cat := NewCatByName("狗子")
cat.Name = "Cat" // 修改的是Cat类的Name
fmt.Println(cat.Name) // Cat
fmt.Println(cat.Animal.Name) // 狗子:Animal的Name保持不变
}
这种将一个struct嵌到另一个struct的方式,称作组合。
组合
组合是一种机制,可将较简单的类型组合起来创建更复杂的类型,是OOP编程中的一个基本概念,它允许通过组合较小、更简单的类型来生成复杂的类型;
这一特性,提升了代码复用率,并使得构建更灵活和模块化的程序成为可能。
Go中的组合是通过结构体嵌入来实现的,这使得新创建的类型可继承嵌入类型的字段和方法,并且可以像使用自己的字段和方法一样使用它们。
组合与伪继承的区别
- 如果一个struct嵌套了另一个匿名的struct,那么这个结构可直接访问匿名结构体的字段和方法,即,伪继承;
- 如果当前struct同时嵌套了多个匿名结构体,那么这个结构可直接访问多个匿名结构体的字段和方法,从而实现多重继承;
如果一个struct在嵌套另一个结构体时指定了【字段名】,即被嵌入的结构体是【有名】的,那就不能继承;
type Cat struct {
Ani *Animal // 我们给被嵌入的Animal指定字段名: Ani
}
cat := NewCatByName("狗子")
cat.Name = "Cat" // 如此调用会报错,需要使用 cat.Ani.Name
接口与抽象类型
OOP语言还有一个很重要的概念,接口
。
接口(interface)定义了一个对象的行为规范,但,只定义不实现,由具体的对象来实现规范的细节。
先来想想为什么我们需要这么一个玩意儿。
还是参考上面的示例,假如现在有一个需求是输出每种动物的叫声,那么可以这么做:
type Cat struct{}
type Dog struct{}
func (c *Cat) Say() {
fmt.Println("喵喵喵")
}
func (d *Dog) Say() {
fmt.Println("汪汪汪")
}
func main() {
// go中的new和其他语言的new用法大差不差,只不过它返回的是指针类型
// 指针类型的struct和值类型的用法没啥区别,算是go内置的一种语法糖
new(Cat).Say()
new(Dog).Say()
}
又假如,需求做大了,动物种类扩充到100种了,是不是要写100个Say()
?
这种方式属于正向思维:即先构建出具体的猫、狗,然后输出对应的猫、狗声音。
现在采用逆向思维反推一下:先构建一个泛指的动物,这个动物可以输出声音,发声时将具体的动物种类赋予泛指的动物,是不是就可以了?
这就要用到 接口 这种东西了, 下面我们看一下接口在go中的实现方式:
// 定义一个 IAnimal 接口(一般以 I 开头标识这是一个接口类型),其中定义了 Say 方法
type IAnimal interface {
Say()
}
type Cat struct{}
type Dog struct{}
// 一个对象只要全部实现了接口中的方法,就是实现了这个接口。简言之:接口提供了一份必须要实现的方法列表。
func (c *Cat) Say() {
fmt.Println("喵喵喵")
}
func (d *Dog) Say() {
fmt.Println("汪汪汪")
}
// 接口实现之后,我们可以定义一个 泛型的接收具体动物种类并输出动物声音 的函数用来完成需求
func saySometh(x IAnimal) {
x.Say()
}
func main() {
// 总之,只需要上游将具体动物种类作为入参,即可达到目的
saySometh(new(Cat)) // OR:saySometh(new(Dog))
}
关于go的接口
在Go语言中接口(interface)是一种数据类型
,一种抽象的数据类型
(为了保护你的Go生涯,请牢记接口(interface)是一种数据类型
)。
接口,和其他的派生数据类型,如 指针、数组、map、struct、Channel 平级;
接口 是一组 method 的集合,提供了一份必须要实现的方法列表,不关心属性(数据),只关心行为(方法)。
就个人理解来看,接口就是逆向推导思维;
譬如,一台洗衣机可以洗衣、甩干;接口就是将这个逻辑反过来,只要一台机器能洗衣、甩干,就是洗衣机(不严谨,凑合看)。
万能的空接口
interface{} 类型(空接口)是一个没有任何方法的接口。
由于没有 implements 关键字,因此,所有类型至少实现了0个方法,并且自动满足一个接口
,也就是说,go中的所有数据类型,天然的实现了空接口
。
这就意味着,如果以 空接口(interface{})作为某个函数的入参类型,那么这个函数就可以接收任意类型的参数:
// 定义一个以 空接口 作为入参的函数,用于输出接收到的变量的 类型
func receiveAnyParam(anyone interface{}) {
fmt.Println(reflect.TypeOf(anyone).String())
}
func main() {
receiveAnyParam("字符串吖") // string
receiveAnyParam([]int{111}) // []int
receiveAnyParam(map[string]string{"k": "v", "k22": "v22"}) // map[string]string
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。