在 Golang 中,結構體(Struct)和 接口(Interface)是實現封裝和多態的兩個重要工具。透過結構體,你可以將相關的數據和行為組織在一起,形成更具結構導向(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的使用方式很相似。
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一樣使用這些常量。
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) 是 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)來檢查和轉換空接口中的具體類型。
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 自行車發動了
「應該將大而全的接口拆分成多個小而專一的接口」,讓使用者只依賴那些與他們具體需求相關的功能,而不應該被迫依賴他們不需要的功能。
接口分離原則的核心理念是:
**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),我們了解了如何將大而全的接口拆分為專一的小接口,提升系統的靈活性和可維護性。