目標
了解類型定義不僅告訴編譯器如何在內存中存儲和處理數據,還對程序設計產生深遠影響:
- 內存結構:類型決定了變量的底層存儲(比如占用多少字節、內存布局等)。
- 操作符與方法集:類型決定了哪些內置運算符適用于它,以及能否為該類型定義方法,從而擴展它的行為。
- 語義區分:通過定義新類型,即使它們的底層類型相同,也能明確區分不同概念,避免混用(例如溫度單位、索引、文件描述符等)。
- 可讀性與安全性:使用命名類型可以讓代碼語義更明確,從而降低出錯的風險。
概念
- 每個變量或表達式都有一個類型,類型描述了值的屬性,如數據占用的內存大小、內部結構、支持哪些操作以及關聯的方法集等。
- 一個
int
類型的變量可能用于表示索引、時間戳或月份;一個float64
類型的變量可能表示速度或溫度。盡管底層都是數字,但語義上卻截然不同。 - 內存與數據表示:每個數據類型在內存中的表示方式(如整數占用 32 位或 64 位)由其底層類型決定。命名類型會沿用相同的存儲方式,但在類型系統中賦予它新的含義。
- 方法與接收者
- **方法的定義:**在 Go 中,可以為任意命名類型定義方法,使得該類型的值具有特定行為(如格式化輸出)。
- 調用示例:fmt.Printf 等函數會自動調用類型的 String 方法,從而定制輸出格式。
- 靜態分派:fmt.Printf 等函數會自動調用類型的 String 方法,從而定制輸出格式。
要點
類型聲明語句
type 新類型名稱 底層類型
這條語句創建一個新的類型名稱,其底層結構與現有類型相同,但在類型系統中被認為是不同的。例如:
type Celsius float64 // 攝氏溫度類型
type Fahrenheit float64 // 華氏溫度類型
即使 Celsius 和 Fahrenheit 都基于 float64,它們是兩個不同的類型,防止你無意中混用不同溫度單位的數據。
- 命名規則與導出
- 如果新類型名稱的首字符大寫,則表示該類型對包外可見(導出)。
- Go 語言中關于 Unicode 字母的規則也使得中文命名默認不能導出(Go 1 的規則),但在將來的 Go 2 中可能會調整。
- 用途
- 語義區分:通過定義不同的命名類型(即使底層相同)可以讓程序更安全,不會將代表不同概念的值誤用在一起。
- 簡化代碼:對于復雜或冗長的類型,用一個簡單的名稱可以使代碼更清晰易讀。
類型轉換操作
-
操作形式
類型轉換寫作
T(x)
,它不會改變 x 的實際值,只是將它“貼上”新的類型標簽var f Fahrenheit = CToF(BoilingC) fmt.Println(Celsius(f)) // 將 f 轉換為 Celsius 類型
- 限制:只有當兩個類型的底層類型完全相同時,或者都是指向相同底層結構的指針時,才能進行轉換。
-
作用
通過類型轉換,可以將同樣底層數據但語義不同的值顯式轉換,使得運算和比較變得類型安全。例如
var c Celsius var f Fahrenheit // c == f // 編譯錯誤:類型不匹配 fmt.Println(c == Celsius(f)) // 顯式轉換后可以比較
類型與方法
-
方法集
命名類型不僅是一個新的數據類型,還可以為該類型定義方法,這使得它擁有特定的行為。
func (c Celsius) String() string {return fmt.Sprintf("%g°C", c) }
這段代碼為 Celsius 類型定義了一個 String 方法,在調用 fmt.Printf 時會自動調用該方法來格式化輸出。(因為 fmt 包會自動調用 String 方法)(具體一點說:當一個類型實現了 String() string 方法后,它就滿足了 fmt 包定義的 Stringer 接口。fmt 包在格式化輸出時,會檢查傳入的值是否實現了該接口,如果實現了,就自動調用 String() 方法來獲取該值的字符串表示。)
c := FToC(212.0) fmt.Println(c.String()) // 輸出 "100°C" fmt.Printf("%v\n", c) // 也輸出 "100°C",因為 fmt 包會自動調用 String 方法
為類型定義方法不僅增加了代碼的可讀性,也讓類型更具語義。
如何定義 Celsius 與 Fahrenheit 兩個命名類型及它們之間的轉換和方法
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconvimport "fmt"type Celsius float64 // 定義攝氏溫度類型
type Fahrenheit float64 // 定義華氏溫度類型// 常量聲明
const (AbsoluteZeroC Celsius = -273.15 // 絕對零度(攝氏)FreezingC Celsius = 0 // 冰點(攝氏)BoilingC Celsius = 100 // 沸點(攝氏)
)// CToF 將攝氏溫度轉換為華氏溫度
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32)
}// FToC 將華氏溫度轉換為攝氏溫度
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9)
}// Celsius 的 String 方法,使其輸出更友好
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c)
}
- Celsius 類型的 String 方法為其提供了定制格式,當使用 fmt 包打印時,會自動調用此方法。
類型比較與運算
-
支持內置運算符
因為 Celsius 和 Fahrenheit 的底層類型都是 float64,所以它們可以使用底層類型的算術和比較運算符。例如:
fmt.Printf("%g\n", BoilingC-FreezingC) // 輸出 "100" boilingF := CToF(BoilingC) fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // 輸出 "180"
注意,直接將 Fahrenheit 與 Celsius 進行運算會導致編譯錯誤,因為它們是不同的命名類型。
-
比較運算
只有相同類型之間才能進行直接比較:
var c Celsius var f Fahrenheit fmt.Println(c == 0) // "true",0 會被視為 Celsius 類型的零值 fmt.Println(f >= 0) // "true",同理,0 對于 Fahrenheit 也是零值 // fmt.Println(c == f) // 編譯錯誤:類型不匹配 fmt.Println(c == Celsius(f)) // 正確:通過類型轉換后比較
總結
- 類型定義描述了變量的內存表示、支持的運算符和方法集,同時區分不同的語義。
- 將不同的概念定義為不同的命名類型,即使它們底層相同,也能在編譯期防止錯誤的混用。(例如,不小心將溫度與文件描述符混用可能引發錯誤,通過定義不同類型就可以避免這種情況。)
- 類型轉換
T(x)
只改變 x 的類型標簽而不改變底層數據。轉換要求兩個類型有相同的底層類型或兼容的結構。 - 方法與類型行為:為命名類型定義方法(如 String 方法),使得在打印和其它操作時可以自動調用,增強了類型的表現力。
- 新類型繼承了底層類型的算術與比較操作,但必須保證操作數類型一致。