Skip to content

類型

在之前的數據類型的小節中已經簡單了介紹過了 Go 中的所有內置的數據類型,這些內置的基礎類型,是後續自定義類型的基礎。Go 是一個典型的靜態類型語言,所有變量的類型都會在編譯期確定好,並且在整個程序的生命周期都不會再改變,這一小節會簡單的介紹下 Go 的類型系統和基本使用。

靜態強類型

Go 是一個靜態強類型語言,靜態指的是 Go 所有變量的類型早在編譯期間就已經確定了,在程序的生命周期都不會再發生改變,盡管 Go 中的短變量聲明有點類似動態語言的寫法,但其變量類型是由編譯器自行推斷的,最根本的區別在於它的類型一旦推斷出來後不會再發生變化,動態語言則完全相反。所以下面的代碼完全無法通過編譯,因為 a 是int類型的變量,不能賦值字符串。

go
func main() {
  var a int = 64
  a = "64"
  fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}

強類型則指的是在程序中執行嚴格的類型檢查,如果出現類型不匹配的情況時,會立即告訴程序員不應該這麼做,而不是像動態語言一樣去嘗試推斷可能的結果。所以下面的代碼無法通過編譯,因為兩者類型不同,無法進行運算。

go
func main() {
  fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}

類型後置

Go 為什麼要把類型聲明放在後面而不是前面,很大程度上是從 C 語言吸取了教訓,拿官方的一個例子展示效果,這是一個函數指針

c
int (*(*fp)(int (*)(int, int), int))(int, int)

說實話不認真看很難知道這是一個什麼類型,在 Go 中類似的寫法如下

go
f func(func(int,int) int, int) func(int, int) int

Go 的聲明方式始終遵循名字在前面,類型在後面的原則,從左往右讀,大概第一眼就可以知道這是一個函數,且返回值為func(int,int) int。當類型變得越來越復雜時,類型後置在可讀性上要好得多,Go 在許多層面的設計都是為了可讀性而服務的。

類型聲明

在 Go 中通過類型聲明,可以聲明一個自定義名稱的新類型,聲明一個新類型通常需要一個類型名稱以及一個基礎類型,簡單的例子如下:

go
type MyInt int64

在上述類型聲明中,通過type關鍵字聲明了一個基礎類型為int64名為MyInt的類型。在 Go 中,每一個新聲明的類型都必須有一個與之對應的基礎類型,且類型名稱不建議與已有的內置標識符重復。

go
type MyInt int64

type MyFloat64 float64

type MyMap map[string]int

// 可以通過編譯,但是不建議使用,這會覆蓋原有的類型
type int int64

通過類型聲明的類型都是新類型,不同的類型無法進行運算,即便基礎類型是相同的。

go
type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)
go
invalid operation: f1 + f (mismatched types MyFloat64 and float64)

類型別名

類型別名與類型聲明則不同,類型別名僅僅只是一個別名,並不是創建了一個新的類型,簡單的例子如下:

go
type Int = int

兩者是都是同一個類型,僅僅叫的名字不同,所以也就可以進行運算,所以下例自然也就可以通過編譯。

go
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)
3

類型別名對於一些特別復雜的類型有很大的用處,例如現在有一個類型map[string]map[string]int,這是一個二維 map,現有一個函數參數是map[string]map[string]int類型,如下

go
func PrintMyMap(mymap map[string]map[string]int) {
   fmt.Println(mymap)
}

這種情況下,就沒有必要使用類型聲明了,因為前者是聲明了一個新的類型,無法作為該函數的參數,使用類型別名後的例子如下

go
type TwoDMap = map[string]map[string]int

func PrintMyMap(mymap TwoDMap) {
   fmt.Println(mymap)
}

使用類型別名後看起來會簡潔一些。

TIP

內置類型any就是interface{}的類型別名,兩者完全等價,僅僅叫法不一樣。

類型轉換

在 Go 中,只存在顯式的類型轉換,不存在隱式類型轉換,因此不同類型的變量無法進行運算,無法作為參數傳遞。類型轉換適用的前提是知曉被轉換變量的類型和要轉換成的目標類型,例子如下:

go
type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)
0.30000000000000004

通過顯式的將MyFloat64 轉換為float64類型,才能進行加法運算。類型轉換的另一個前提是:被轉換類型必須是可以被目標類型代表的(Representability),例如int可以被int64類型所代表,也可以被float64類型代表,所以它們之間可以進行顯式的類型轉換,但是int類型無法被stringbool類型代表,因為也就無法進行類型轉換。

TIP

關於代表(Representabilitsy)的定義可以前往參考手冊 - Representability以了解更多細節。

即便兩個類型可以相互代表,類型轉換的結果也不總是正確的,看下面的一個例子:

go
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))
1 0

num1被正確的轉換為了int32類型,但是num2並沒有。這是一個典型的數值溢出問題,int32能夠表示 31 位整數,int8只能表示 7 位整數,高精度整數在向低精度整數轉換時會拋棄高位保留低位,因此num1的轉換結果就是 0。在數字的類型轉換中,通常建議小轉大,而不建議大轉小。

在使用類型轉換時,對於一些類型需要避免歧義,例子如下:

go
*Point(p) // 等價於 *(Point(p))
(*Point)(p)  // 將p轉換為類型 *Point
<-chan int(c)    // 等價於 <-(chan int(c))
(<-chan int)(c)  // 將c轉換為類型  <-chan int
(func())(x)      // 將x轉換為類型 func()
(func() int)(x)  // 將x轉換為類型 func() int

類型斷言

類型斷言通常用於判斷某一接口類型的變量是否屬於某一個類型,示例如下

go
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
   fmt.Println(intVal)
} else {
   fmt.Println("error type")
}
1

由於interface{}是空接口類型,空接口類型可以代表所有的類型,但是int類型無法代表interface{}類型,所以無法使用類型轉換。而類型斷言就可以判斷其底層類型是否為想要的類型,類型斷言語句有兩個返回值,一個是類型轉換過後的值,另一個是轉換結果的布爾值。

類型判斷

在 Go 中,switch語句還支持一種特殊的寫法,通過這種寫法可以根據不同的case做出不同的邏輯處理,使用的前提是入參必須是接口類型,示例如下:

go
var a interface{} = 2
switch a.(type) {
    case int: fmt.Println("int")
    case float64: fmt.Println("float")
    case string: fmt.Println("string")
}
int

TIP

通過unsafe包下提供的操作,可以繞過 Go 的類型系統,就可以做到原本無法通過編譯的類型轉換操作。

Golang學習網由www.golangdev.cn整理維護