Go語言結構體(struct)物件導向程式設計進階篇(封裝,繼承和多型)

尹正杰發表於2024-07-30

                                              作者:尹正傑

版權宣告:原創作品,謝絕轉載!否則將追究法律責任。

目錄
  • 一.Go語言的封裝(encapsulation)實現
    • 1.什麼是封裝(encapsulation)
    • 2.封裝(encapsulation)的好處
    • 3.golang如何實現封裝(encapsulation)
    • 4.程式碼實現
      • 4.1 程式碼組織結構
      • 4.2 建立go.mod檔案
      • 4.3 dongman.go
      • 4.4 main.go
  • 二.Go語言的繼承(inheritance)實現
    • 1.繼承(inheritance)概述
    • 2.繼承案例程式碼實現
    • 3.Golang支援多繼承
    • 4.巢狀匿名結構體指標
    • 5.組合模式(巢狀結構體)並非繼承
    • 6.巢狀結構體的欄位名衝突
  • 三.Go語言的多型(polymorphic)實現
    • 1.多型概述
    • 2.多型案例
  • 四.結構體記憶體佈局
    • 1 結構體佔用一塊連續的記憶體
    • 2 空結構體
    • 3 記憶體對齊[瞭解即可]

一.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/

相關文章