Golang语言结构体(struct)面向对象编程进阶篇(封装,继承和多态)

简介: 这篇文章是关于Go语言中结构体(struct)面向对象编程进阶篇的教程,涵盖了Go语言如何实现封装、继承和多态,以及结构体内存布局的相关概念和案例。

                                              作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。

一.Go语言的封装(encapsulation)实现

1.什么是封装(encapsulation)

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起。

数据被保护在内部,程序的其他包只有通过被授权的操作方法,才能对字段进行操作。

2.封装(encapsulation)的好处

- 1.隐藏实现细节;

- 2.可以对数据进行校验,保证安全合理;

3.golang如何实现封装(encapsulation)

- 1.建议将结构体,字段(属性)的首字母小写(其他包不能使用,类似private,实际开发不小写也可能,因为封装没有那么严格);

- 2.给结构体所在的包提供一个工程模式函数,首字母大写(类似于一个构造函数);

- 3.提供一个首字母大写的set方法(类似于其他语言的public),用于对属性判断并赋值

- 4.提供一个首字母大写的Get方法(类似于其他语言的publiic),用于获取属性的值;

4.代码实现

4.1 代码组织结构

如上图所示,代码组织结构

4.2 创建go.mod文件

yinzhengjie@bogon 12-encapsulation % go mod init yinzhengjie-fengzhuang
go: creating new go.mod: module yinzhengjie-fengzhuang
yinzhengjie@bogon 12-encapsulation % 
yinzhengjie@bogon 12-encapsulation % ls
go.mod
yinzhengjie@bogon 12-encapsulation % 
yinzhengjie@bogon 12-encapsulation % cat go.mod 
module yinzhengjie-fengzhuang

go 1.22.4
yinzhengjie@bogon 12-encapsulation %

4.3 dongman.go

package dongman

import "fmt"

type dongMan struct {
    Name string
    // 此处为故意将age,hobby字段设置为小写,这意味着其他包无法直接访问这两个属性。
    age    int
    hobby  []string
    Leader string
}

func (d dongMan) String() string {
    return fmt.Sprintf("[%s]的男主是[%s],在[%d]岁时修炼到元婴,我的爱好是: %s", d.Name, d.Leader, d.age, d.hobby)
}

// 定义工程模式函数,相当于其他Java和Python等编程的构造器
func NewDongMan(name string, age int, leader string, hobby []string) *dongMan {
    return &dongMan{
        Name:   name,
        age:    age,
        Leader: leader,
        hobby:  hobby,
    }
}

// 定义set方法,对age字段进行封装,因为在方法中可以添加一系列的限制操作,确保被封装字段的安全合理性
func (d *dongMan) SetAge(age int) {
    // 通过set方法,我们可以设置age的范围,否则外部就可以直接对age字段进行赋值
    if age > 0 && age < 1000 {
        d.age = age
    } else {
        fmt.Printf("元婴期修士寿命范围在0~1000岁,您传入的[%d]不合法\n", age)
    }
}

// 定义get方法,用于外部包获取隐藏的字段
func (d *dongMan) GetAge() int {
    return d.age
}

4.4 main.go

package main

import (
    "fmt"

    "yinzhengjie-fengzhuang/dongman"
)

func main() {

    // 创建dongMan结构体
    d := dongman.NewDongMan("《凡人修仙传》", 217, "韩立", []string{"养灵虫", "制傀儡", "炼丹", "修炼", "阵法"})

    fmt.Println(d)

    // 跨包无法访问小写字母的属性字段
    // d.age = 400
    // d.hobby = []string{"韩跑跑", "捡破烂"}

    // 由于我们将age属性封装起来了,想要访问该字段,则需要通过SetAge方法进行修改,如果字段不合法则不会设置成功哟~
    // d.SetAge(2000) 
    d.SetAge(300)

    // 由于我们将age属性封装起来了,想要访问该字段,则需要通过GetAge方法进行查看
    fmt.Printf("我是[%s]男主[%s],今年[%d]岁~\n", d.Name, d.Leader, d.GetAge())

}

二.Go语言的继承(inheritance)实现

1.继承(inheritance)概述

当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法,其他的结构体不需要重新定义这些属性和方法,只需嵌套一个匿名结构体即可。

换句话说, 在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,这就是所谓的继承。

如上图所示,继承的优点就是提高了代码的复用性和扩展性,多个结构体无需重复定义属性和方法,仅需关系各自结构体的方法即可。

Golang使用继承注意事项:
    - 1.结构体可以使用嵌套匿名结构体所有的字段和方法,包括首字母大写或者小写的字段,方法,都可以使用;
    - 2.匿名字段结构体字段访问可以简化;
    - 3.当结构体和匿名结构体有相同的字段或者方法时,编译器采用"就近访问"原则访问,如系统访问匿名结构体的字段和方法,可以通过匿名结构体来区分;
    - 4.Golang中支持多继承,如一个结构体嵌套了多个匿名结构体,那么该结构体可以访问直接嵌套的你们结构体的字段和方法,从而实现了多重继承;
    - 5.如嵌入的匿名结构体有相同的字段名或者方法名称,则在访问时,需要通过匿名结构体类型名来区分;
    - 6.结构体的匿名字段可以是基础数据类型,调用时基于该基础数据类型调用即可;
    - 7.在创建嵌套匿名结构体变量(实例)时,可以直接指定各个匿名结构体字段的值;
    - 8.嵌入匿名结构体的指针也是可以的;
    - 9.结构体的字段可以是结构体类型的(组合模式,嵌套结构体),但这种写法并不属于继承关系,只是属于该结构体的一个字段的类型而已;


温馨提示:
    为了保证代码的简洁性,建议大家尽量不使用多重继承,很多语言就将多重继承去除了,但是Go中保留了。

2.继承案例代码实现

package main

import "fmt"

// Animal结构体为表示动物,是其他结构体的"父结构体"
type Animal struct {
    Name   string
    Age    int
    Weight float64
}

// 给Animal绑定Speark方法
func (a *Animal) Speark() {
    fmt.Printf("%s又开始叫唤了...", a.Name)
}

// 给Animal绑定Show方法
func (a *Animal) Show() {
    fmt.Printf("%s的今年%d岁,体重是%.2fkg\n", a.Name, a.Age, a.Weight)
}

// Bird结构体表示鸟,属性字段继承自Animal
type Bird struct {
    // 为了复用性,体现继承思维,加入匿名结构体
    Animal
}

// 为Bird结构体绑定特有的方法
func (b *Bird) Fight() {
    fmt.Printf("快看,%s又飞起来啦~\n", b.Name)
}

// Dog结构体表示狗,属性字段继承自Animal
type Dog struct {
    Animal
}

// 定义Dog结构体特有的方法
func (d *Dog) Run() {
    fmt.Printf("%s狗子,跑的真快\n", d.Name)
}

func (d *Dog) Jump() {
    fmt.Printf("%s狗子,跳的好高\n", d.Name)
}

type Cat struct {
    Animal
}

func (c *Cat) Scratch() {
    fmt.Printf("%s猫咪,又开始抓人了\n", c.Name)
}

func main() {
    bird := &Bird{}
    bird.Animal.Name = "小黄"
    bird.Animal.Age = 1
    bird.Animal.Weight = 0.2

    dog := &Dog{}
    dog.Animal.Name = "雪花"
    dog.Animal.Age = 3
    dog.Animal.Weight = 4

    cat := &Cat{}
    cat.Animal.Name = "大花"
    cat.Animal.Age = 4
    cat.Animal.Weight = 2

    bird.Speark()
    bird.Show()
    bird.Fight()

    dog.Speark()
    dog.Show()
    dog.Run()
    dog.Jump()

    cat.Speark()
    cat.Show()
    cat.Scratch()

}

3.Golang支持多继承

package main

import "fmt"

type Father struct {
    Name string
    Age  int
}

type Mother struct {
    Name string
}

type Son struct {
    Name string
    // 结构体的匿名字段可以是基础数据类型,这种没有名字的字段就称为匿名字段,调用时基于该基础数据类型调用即可;
    //这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名;
    // 结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
    int
    // 继承多个结构体,尽管Go语言支持多继承,但推荐少用。很多语言就将多重继承去除了,因为容易造成逻辑混乱。
    Father
    Mother
}

func (f *Father) DriveCar() {
    fmt.Printf("%s开车很稳~\n", f.Name)
}

func (m *Mother) Sing() {
    fmt.Printf("%s唱歌很好听~\n", m.Name)
}

func (s *Son) Dance() {
    fmt.Printf("%s跳舞很好看\n", s.Name)
}

func main() {

    // 构建Son结构体实例
    // s := Son{"唐三", 18, Father{"唐昊", 30}, Mother{"阿银"}}

    s := Son{
        "唐三",
        18,
        // 在创建嵌套匿名结构体变量(实例)时,可以直接指定各个匿名结构体字段的值;
        Father{
            Name: "唐昊",
            Age:  30,
        },
        Mother{
            Name: "阿银",
        },
    }

    fmt.Printf("s = %v\n", s)

    // 通过Son结构体实例的确可以调用多个继承结构体的方法
    s.Sing()
    s.Dance()
    s.DriveCar()

    // 如嵌入的匿名结构体有相同的字段名或者方法名称,则在访问时,需要通过匿名结构体类型名来区分;
    fmt.Printf("[%s]的今年[%d]岁, 父亲是: [%s], 今年[%d]岁, 母亲是: [%s]\n", s.Name, s.int, s.Father.Name, s.Age, s.Mother.Name)
}

4.嵌套匿名结构体指针

package main

import "fmt"

type Father struct {
    Name string
    Age  int
}

type Mother struct {
    Name string
}

type Son struct {
    Name string
    int
    // 嵌入匿名结构体的指针也是可以的
    *Father
    *Mother
}

func (f *Father) DriveCar() {
    fmt.Printf("%s开车很稳~\n", f.Name)
}

func (m *Mother) Sing() {
    fmt.Printf("%s唱歌很好听~\n", m.Name)
}

func (s *Son) Dance() {
    fmt.Printf("%s跳舞很好看\n", s.Name)
}

func main() {

    // 构建Son结构体实例
    s := Son{"唐三", 18, &Father{"唐昊", 30}, &Mother{"阿银"}}

    fmt.Printf("s = %v\n", s)

    s.Sing()
    s.Dance()
    s.DriveCar()

    fmt.Printf("[%s]的今年[%d]岁, 父亲是: [%s], 今年[%d]岁, 母亲是: [%s]\n", s.Name, s.int, s.Father.Name, s.Age, s.Mother.Name)
}

5.组合模式(嵌套结构体)并非继承

package main

import (
    "fmt"
)

type Address struct {
    Province string
    City     string
}

// 一个结构体中可以嵌套包含另一个结构体或结构体指针,我们也称之为"组合模式"。
type User struct {
    Name   string
    Gender string
    // 结构体的字段可以是结构体类型的(组合模式),但这种写法并不属于继承关系,只是属于该结构体的一个字段的类型而已;
    Address Address
}

func main() {
    user1 := User{
        Name:   "JasonYin",
        Gender: "男",
        Address: Address{
            Province: "陕西",
            City:     "安康",
        },
    }
    fmt.Printf("user1=%#v\n", user1)
}

6.嵌套结构体的字段名冲突

package main

import (
    "fmt"
)

// Address 地址结构体
type Address struct {
    Province   string
    City       string
    CreateTime string
}

// Email 邮箱结构体
type Email struct {
    Account    string
    CreateTime string
}

// User 用户结构体
type User struct {
    Name   string
    Gender string
    Address
    Email
}

func main() {
    var u1 User
    u1.Name = "Jason Yin"
    u1.Gender = "男"
    u1.Province = "陕西"
    u1.City = "安康"
    u1.Account = "y1053419035@qq.com"
    // 由于2个匿名字段都有CreateTime字段,因此无法省略匿名字段哟!
    // u1.CreateTime = "2020"

    // 嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
    u1.Address.CreateTime = "2021"
    u1.Email.CreateTime = "2025"

    fmt.Println(u1)
}

三.Go语言的多态(polymorphic)实现

1.多态概述

变量(实例)具有多种形态,在Go语言中,多态特征是通过接口实现的。可以按照统一的接口来调用不同的接口实现。这时接口变量就呈现出不同的形态。

2.多态案例

package main

import "fmt"

// 定义SayHi接口
type SayHi interface {
    SayHello()
}

type Chinese struct {
    Name string
}

type American struct {
    Name string
}

func (c Chinese) SayHello() {
    fmt.Printf("你好,我的名字是: %s, 很高兴认识你。\n", c.Name)
}

func (a American) SayHello() {
    fmt.Printf("Hi,My name is %s, And you ?\n", a.Name)
}

// 定义一个函数,专门用来各国人打招呼的函数,接受具备SayHi接口能力的变量
// 此处的s(多态参数)可以通过上下文来识别具体是什么类型的实例,此时就体现出"多态"
func greet(s SayHi) {
    s.SayHello()
}

func main() {
    // 定义多态数组
    var array [3]SayHi

    array[0] = Chinese{"女娲"}
    array[1] = American{"超人"}
    array[2] = Chinese{"夸父"}

    fmt.Println(array)

    // 遍历接口,调用接口,体现出多态的效果
    for _, item := range array {
        greet(item)
    }
}

四.结构体内存布局

1 结构体占用一块连续的内存

package main

import (
    "fmt"
    "unsafe"
)

// 结构体占用一块连续的内存。
type test struct {
    a int8
    b int8
    c int8
    d int8
}

func main() {
    n := test{
        11, 22, 33, 44,
    }
    fmt.Printf("n.a %p\n", &n.a)
    fmt.Printf("n.b %p\n", &n.b)
    fmt.Printf("n.c %p\n", &n.c)
    fmt.Printf("n.d %p\n", &n.d)

    // 查看占用的空间大小
    fmt.Println(unsafe.Sizeof(n))
}

2 空结构体

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // 空结构体是不占用空间的。
    var v struct{}

    // 查看占用的空间大小
    fmt.Println(unsafe.Sizeof(v))
}

3 内存对齐[了解即可]

关于Go语言中的内存对齐推荐阅读:
    https://www.liwenzhou.com/posts/Go/struct-memory-layout/
目录
相关文章
|
12天前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
43 4
Golang语言之管道channel快速入门篇
|
12天前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
40 3
Golang语言之gRPC程序设计示例
|
12天前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
36 4
|
12天前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
20 3
Golang语言之Prometheus的日志模块使用案例
|
12天前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
36 4
Golang语言文件操作快速入门篇
|
12天前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
35 3
|
12天前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
23 4
Golang语言goroutine协程篇
|
12天前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
16 3
Golang语言之函数(func)进阶篇
|
12天前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
17 2
Golang语言之函数(func)基础篇
|
12天前
|
Go
Golang语言之映射(map)快速入门篇
这篇文章是关于Go语言中映射(map)的快速入门教程,涵盖了map的定义、创建方式、基本操作如增删改查、遍历、嵌套map的使用以及相关练习题。
21 5