iT邦幫忙

2024 iThome 鐵人賽

DAY 9
1
Modern Web

Go 快 Go 高效: 從基礎語法到現代Web應用開發系列 第 9

【Day09】封裝與多態性 I | Struct & iota & Interface

  • 分享至 

  • xImage
  •  

在 Golang 中,結構體(Struct)接口(Interface)是實現封裝和多態的兩個重要工具。透過結構體,你可以將相關的數據和行為組織在一起,形成更具結構導向(Struct-Oriented)的設計。而接口則提供了一種輕量的多態機制,讓我們能夠為不同類型提供統一的行為定義。

結構導向(Struct-Oriented)

結構體(Struct)是 Golang 中的基本資料類型之一,它允許我們將相關聯的數據組裝在一起,形成一個有意義的對象。在結構導向設計中,我們會將一組相關的數據與行為封裝在一個結構體內,這有助於維護代碼的清晰性與可讀性。

  • 多個結構體組裝
package main

import "fmt"

// 定義車子結構體
type Car struct {
	Brand string
	Model string
	Year  int
}

// 定義司機結構體
type Driver struct {
	Name string
	Age  int
	Car  Car // 將 Car 結構體組裝進 Driver 結構體
}

func main() {
	// 初始化 Car 和 Driver
	myCar := Car{Brand: "Toyota", Model: "Corolla", Year: 2020}
	driver := Driver{Name: "John", Age: 35, Car: myCar}

	fmt.Printf("司機 %s 駕駛 %d 年的 %s %s\n", driver.Name, driver.Car.Year, driver.Car.Brand, driver.Car.Model)
}
</* Output: */>
司機 John 駕駛 2020 年的 Toyota Corolla

我們將 Car 結構體組裝進了 Driver 結構體,這展示了如何將多個結構體組合在一起,形成一個更有結構導向的設計。每個結構體都專注於其相關的數據:Car 專注於車輛屬性,Driver 專注於司機資訊,並且組裝了與司機相關的車輛資料。


使用 iota 實現枚舉(Enum)

當我們需要定義一組相關的具名常量時,iota 可以非常方便地生成遞增的整數值,並且可以賦予具體的語義,這與 enum 的使用方式很相似。

package main

import "fmt"

// 使用 iota 定義一組星期的常量
const (
    Sunday = iota // 0
    Monday        // 1
    Tuesday       // 2
    Wednesday     // 3
    Thursday      // 4
    Friday        // 5
    Saturday      // 6
)

func main() {
    fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}

在這裡,iota 自動為這些常量賦值,從 0 開始遞增,這樣就可以像 enum 一樣使用這些常量。

  • 位元操作的 enum
const (
    FlagNone   = 1 << 0  // 1  (二進位: 0001)
    FlagRead   = 1 << 1  // 2  (二進位: 0010)
    FlagWrite  = 1 << 2  // 4  (二進位: 0100)
    FlagExecute = 1 << 3 // 8  (二進位: 1000)
)

這樣的位元操作枚舉允許我們使用位元標誌來表示權限控制,例如可以用 FlagRead | FlagWrite 來表示讀寫權限。

使用 iota 的優勢

  • 自動遞增iota 在每一個新常量定義時會自動遞增,不需要手動指定每個常量的值。
  • 靈活性iota 可以用來定義不僅僅是連續的數字,它還可以結合運算符來實現更多複雜的常量生成邏輯,例如位元操作。

輕量的多態性(Interface)

接口(Interface) 是 Golang 提供的輕量多態機制。透過接口,你可以為不同的結構體定義統一的行為,而不需要具體實現該行為的類型之間有繼承關係。這讓我們可以實現靈活的多態,並保持代碼簡潔。

interface{}

  • 接受任意類型的值

空接口 interface{} 是一個沒有任何方法的接口,因為 Golang 中的接口是基於方法集合的,任何類型都會隱式地實現空接口(因為任何類型都沒有方法)。因此,空接口可以用來接收和存儲任意類型的值。

package main

import "fmt"

func printValue(value interface{}) {
    fmt.Println(value)
}

func main() {
    printValue(123)        // 傳入整數
    printValue("Hello")    // 傳入字串
    printValue(3.14)       // 傳入浮點數
}

在這個範例中,printValue 函數接受一個空接口類型的參數,這意味著我們可以傳入任何類型的值,而 fmt.Println 可以處理各種類型的輸出。這是空接口最基本的用法:處理不同類型的值。

  • 動態類型判斷(Type Assertion)

雖然空接口可以接受任意類型的值,但有時我們需要在使用時將這些值轉換回它們的具體類型。這時候就需要使用動態類型判斷(Type Assertion)來檢查和轉換空接口中的具體類型。

package main

import "fmt"

func checkType(value interface{}) {
    switch v := value.(type) {
    case int:
        fmt.Printf("整數:%d\n", v)
    case string:
        fmt.Printf("字串:%s\n", v)
    case float64:
        fmt.Printf("浮點數:%f\n", v)
    default:
        fmt.Printf("未知類型\n")
    }
}

func main() {
    checkType(123)
    checkType("Hello")
    checkType(3.14)
}

在這裡,我們使用了 value.(type) 語法來進行類型判斷,並依據值的具體類型進行不同的處理。這允許我們動態處理空接口中的不同類型的值。

  • 泛型處理(也可以從下方的延伸閱讀去觀看更多使用範例)

雖然 Golang 在後續版本中引入了泛型(Generics),但在泛型引入之前,interface{} 是實現泛型功能的替代方案之一。透過 interface{},你可以設計出處理多種不同類型的函數或資料結構,從而實現一種輕量級的「泛型」。

package main

import "fmt"

// 通用的處理函數,接受空接口切片
func printSlice(slice []interface{}) {
    for _, value := range slice {
        fmt.Println(value)
    }
}

func main() {
    slice := []interface{}{1, "hello", 3.14}
    printSlice(slice) // 可以傳入不同類型的元素
}

在這個範例中,我們使用 []interface{} 來存儲不同類型的元素,並透過 printSlice 函數處理這些元素。這是 Golang 在泛型出現之前常見的處理方式之一。

注意事項

儘管空接口非常靈活,但也存在一些潛在的缺點:

  • 缺乏類型安全:使用空接口後,Golang 失去了類型檢查的能力,所有類型檢查都必須在運行時進行,這會導致潛在的錯誤。使用 interface{} 時,必須經常進行動態類型斷言或判斷,這樣會增加代碼的複雜度。

  • 性能損耗:由於 interface{} 涉及到動態類型處理,這種靈活性可能會帶來額外的性能開銷,特別是在處理大量數據時。

接口實現多態

// 定義一個接口
type Vehicle interface {
	Start() string
}

// 定義車子結構體
type Car struct {
	Brand string
	Model string
	Year  int
}

// Car 結構體實現 Vehicle 接口
func (c Car) Start() string {
	return fmt.Sprintf("%s %s 發動了", c.Brand, c.Model)
}

// 定義 Bike 結構體
type Bike struct {
	Brand string
}

// Bike 結構體也實現 Vehicle 接口
func (b Bike) Start() string {
	return fmt.Sprintf("%s 自行車發動了", b.Brand)
}

func main() {
	myCar := Car{Brand: "Toyota", Model: "Corolla"}
	myBike := Bike{Brand: "Giant"}

	vehicles := []Vehicle{myCar, myBike} // 使用接口的多態性

	for _, v := range vehicles {
		fmt.Println(v.Start()) // 不需要知道具體的類型,只需調用 Start 方法
	}
}
</* Output: */>
Toyota Corolla 發動了
Giant 自行車發動了

接口分離原則(Interface Segregation Principle, ISP)

「應該將大而全的接口拆分成多個小而專一的接口」,讓使用者只依賴那些與他們具體需求相關的功能,而不應該被迫依賴他們不需要的功能。

接口分離原則的核心理念是:

  • 接口應該專一:每個接口應該只包含與特定功能或責任相關的方法,使用者應該只依賴自己需要的功能。
  • 避免臃腫的接口:不應設計過於龐大的接口,因為這會讓實現這些接口的類型負擔過多無關的責任。
  • 避免接口污染:一個接口不應該強迫實現者去實現那些它們不需要的方法。

**ISP 的好處 : **

  • 提高可讀性和可維護性:小而專一的接口使代碼更加簡潔,容易理解和維護。
  • 靈活性更高:如果接口過於龐大,當一個類型只需要部分功能時,還得實現不必要的功能。這會導致過度設計和額外的維護負擔。ISP 幫助避免這種情況,保持系統靈活性。
  • 避免變更影響:當接口變更時,只有真正依賴該接口的實現會受到影響,其他不依賴該接口的部分不會受到連鎖影響。
// FIXME: - 臃腫的接口,包含了所有功能
type Printer interface {
    PrintDocument(doc string)
    ScanDocument(doc string)
    FaxDocument(doc string)
}

type BasicPrinter struct{}

// BasicPrinter 只需要打印功能,但必須實現所有的方法
func (p BasicPrinter) PrintDocument(doc string) {
    fmt.Println("打印文件:", doc)
}

// 即使不需要,還是需要實現其他方法
func (p BasicPrinter) ScanDocument(doc string) {}
func (p BasicPrinter) FaxDocument(doc string) {}
// TODO: - 專一的小接口
type Printer interface {
    PrintDocument(doc string)
}

type Scanner interface {
    ScanDocument(doc string)
}

type Fax interface {
    FaxDocument(doc string)
}

// BasicPrinter 只實現打印功能
type BasicPrinter struct{}

func (p BasicPrinter) PrintDocument(doc string) {
    fmt.Println("打印文件:", doc)
}

// 高階的多功能打印機,可以實現多個接口
type MultiFunctionPrinter struct{}

func (p MultiFunctionPrinter) PrintDocument(doc string) {
    fmt.Println("打印文件:", doc)
}

func (p MultiFunctionPrinter) ScanDocument(doc string) {
    fmt.Println("掃描文件:", doc)
}

func (p MultiFunctionPrinter) FaxDocument(doc string) {
    fmt.Println("傳真文件:", doc)
}

在這裡,我們遵循了接口分離原則:

  • Printer 接口只專注於打印功能。
  • Scanner 接口專注於掃描功能。
  • Fax 接口專注於傳真功能。
  • BasicPrinter 只需要實現 Printer 接口,不必強迫實現其他不相關的功能。而 MultiFunctionPrinter 則可以同時實現所有的接口。

總結

今天我們學到 Golang 中的 結構體(Struct)接口(Interface),並說明了如何透過這兩者實現封裝和多態性。結構體讓我們能夠組織相關數據,而接口提供了輕量的多態機制,使不同類型可以實現統一的行為。我們也討論了 空接口(interface{}) 的用途,包括動態類型判斷和泛型處理。同時,透過 接口分離原則(ISP),我們了解了如何將大而全的接口拆分為專一的小接口,提升系統的靈活性和可維護性。


延伸閱讀


上一篇
【Day08】Golang 基礎語法 | 陣列與切片(Arrays & Slices)
下一篇
【Day10】封裝與多態性 II | 鬆散耦合
系列文
Go 快 Go 高效: 從基礎語法到現代Web應用開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言