在 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)
,我們了解了如何將大而全的接口拆分為專一的小接口,提升系統的靈活性和可維護性。