写在前面:
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
}

后厂村村长
7 声望2 粉丝

Hello, Debug World