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

                                              作者:尹正杰

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

一.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/
posted @ 2024-07-30 01:22  尹正杰  阅读(82)  评论(0编辑  收藏  举报