Go語言學習 十八 類型體系

本文最初發表在我的個人博客,查看原文,獲得更好的閱讀體驗


Go語言中的類型既有預定義的,也允許用戶自定義。正如在Java中我們可以自定義類一樣,在Go中我們可以通過自定義類型來創造新的數據類型。

複合字面量可以爲結構、數組、切片或映射類型構造值,字面量的底層類型必須是以上類型,該規則受語法的強制約束,除非類型以類型名稱給出。元素和鍵的類型必須可分配給字面量類型的相應字段、元素和鍵的類型;沒有額外的轉換。字面量的鍵將解釋爲結構字面量的字段名,數組或切片的索引,或映射的鍵。指定具有相同字段名稱或常量鍵值的多個元素是錯誤的。

一 類型聲明

類型聲明將新的標識符(類型名稱)綁定到類型。類型聲明有兩種形式:別名聲明和類型定義。

1.1 別名聲明

語法:
type 別名(標識符) = 類型

別名只是一個新的標識符,但並未創建新的類型。作爲該類型的別名,自然與該類型是一樣的。
例如,以下示例我們新定義了一個類型別名,其中Time[]int具有相同的類型:

package main

import "fmt"

func main() {
	var a Time

	fmt.Printf("%T", a)
}

type Time = []int // Time是[]int的一個類型別名

可以同時聲明多個類型別名:

type (
	Time  = []int              // Time 和 []int 是相同的類型
	Color = []string           // Color 和 []string 是相同的類型
	Point = struct{ x, y int } // Point 和 struct{ x, y int } 是相同的類型
	A     = Point              // A, Point 和 struct{ x, y int } 是相同的類型
)

1.2 類型定義

與別名聲明不同,類型定義會創建一個新的,不同的類型,其具有與給定類型相同的底層類型和操作。

語法:
type 新類型名稱(標識符) 已定義類型|struct{}

注意與別名聲明的區別,新類型標識符與創建它的類型之間沒有 = 符號。

從語法中可以看出,有兩種方法可以自定義類型:基於已有的類型,使用struct關鍵字。

新類型稱爲已定義類型。它與任何其他類型不同,包括創建它的類型。例如type A int定義了一個新的類型A,其底層就是一個int類型,但是Aint仍然是不同的兩個類型。
示例:

type (
	Point struct{ x, y float64 }  // Point 和 struct{ x, y float64 } 是不同的類型
	polar Point                   // polar 和 Point 也是不同的類型
)

// 新的結構類型
type TreeNode struct {
	left, right *TreeNode
	value *Comparable
}

// 新的接口類型
type Block interface {
	BlockSize() int
	Encrypt(src, dst []byte)
	Decrypt(src, dst []byte)
}

1.2.1 藉助已有類型定義

我們可以基於已定義的類型定義新的類型,以下示例中我們基於uint類型創建的新的計數器類型Counter

package main

import "fmt"

func main() {
	var c Counter
	var i uint = 8

	// c = i // 錯誤: cannot use i (type uint) as type Counter in assignment
	c = 9    // OK.

	fmt.Printf("%T, %T\n", c, i) // main.Counter, uint
	fmt.Printf("%v, %v\n", c, i) // main.Counter, uint

}

type Counter uint // Counter 是一個新的類型

雖然Counter底層就是由uint構成,但它們卻是不同的類型,從打印出的結果也能看出。

另外,由於Go語言中對已定義的類型之間不能做隱式轉換,故上述示例中的第9行代碼如果去掉註釋的話不能編譯通過。

1.2.2 使用struct關鍵字定義

使用struct關鍵字定義的類型稱爲結構類型。結構(struct)就是一組字段(field)的集合。字段名稱可以顯式指定(標識符列表)或隱式指定(內嵌字段)。在結構中,非空白字段名稱必須是唯一的。

// 一個空的結構類型
struct {}

// 7個字段的結構類型
struct {
	x, y int
	u    float32
	// u string // 重複的字段 u
	_ float32 // padding
	_ int     // 空白標識符可以重複
	A *[]int
	F func()
}

注意類型定義與類型的區別。

示例:

package main

import "fmt"

// Point用於表示xy座標軸上的點
type Point struct {
	X int // 字段
	Y int // 字段
}

1.2.3 初始化

定義好之後,我們就可以像其他內置類型一樣來使用它了:

var p Point    // 定義一個點p
fmt.Println(p) // {0 0}

聲明瞭一個變量後,這個變量首先會被初始化爲該類型的零值,對於struct類型的零值,是其各個字段的零值。對於我們定義的上述變量p,其兩個字段會首先初始化爲0。
有2種方法可以爲一個struct變量賦值:結構字面量、點號(.):

結構字面量

形如TypeName{key1: value1, key2: value2, ...}

package main

import "fmt"

type Point struct {
	X int
	Y int
}

func main() {
	p0 := Point{Y: 5}       // 指定鍵名時,可以只給指定的字段賦值,其他缺省爲零值:{0 5}
	p1 := Point{X: 2, Y: 3} // 分別指定每個字段的值:{2 3}
	p2 := Point{Y: 4, X: 3} // 指定字段名的時候,字段順序無所謂:{3 4}
	p3 := Point{12, 13}     // 忽略字段名:{12 13}
	p4 := Point{13, 12}     // 不指定字段名時,順序很重要,要嚴格按照字段聲明的順序給出對應的值:{13 12}
	p5 := Point{}           // 零值,等價於 var p5 Point

	fmt.Println(p0, p1, p2, p3, p4, p5)
}

需要注意的是,使用結構字面量初始化時,如果每個字段單獨一行,則每一行結尾必須要有一個逗號:

p := Point{
	5,
	12,
}

對於結構字面量,適用於以下規則:

  • 鍵必須是在結構體中聲明的字段。
  • 不包含鍵名時,元素順序要嚴格按照結構中字段的聲明順序的順序。
  • 如果任何一個元素有鍵,其他元素都要有。
  • 在給出鍵名的前提下,無需按順序賦值,也無需給全部字段賦值,缺省字段將使用其零值初始化。
  • 字面量也可以忽略全部元素的賦值,這樣的字面量相當於該類型的零值。
  • 不能爲屬於不同包的結構的非導出字段指定元素。

獲取複合字面量的地址會生成一個指向該字面量值初始化的唯一變量的指針。

var pointer *Point = &Point{X: 2, Y: 3}

點號

除了使用上述字面量的方式直接初始化結構類型的字段值,還可以使用點號來訪問或賦值:

package main

import "fmt"

type Point struct {
	X int
	Y int
}

func main() {
	var p Point

	fmt.Println(p) //{0 0}

	p.X = 2
	p.Y = 3
	fmt.Println(p) //{2 3}
}

1.2.4 字段標記

字段聲明時,可以包含一個可選的字符串字面量標記,該標記將作爲該行聲明的所有字段的一個屬性。空字符串標記相當於沒有標記。標記通過反射接口可見。並且是類型標識的一部分,其他情況下可以忽略。

type T struct {
	x, y float64 ""  // 空標記相當於沒有標記
	name string  "任何字符串都可以作爲標記"
	_    [4]byte "ceci n'est pas un champ de structure(法語)"
}

// 表示TimeStamp協議緩衝區的結構。標記定義了協議緩衝區字段的序號;
// 它們遵循反射包概述的慣例
struct {
	microsec  uint64 `protobuf:"1"`
	serverIP6 uint64 `protobuf:"2"`
}

1.3 結構指針

結構字段可以通過結構指針來訪問。

如果我們有一個指向結構體的指針 p,那麼可以通過 (*p).X 來訪問其字段 X。不過這麼寫太麻煩,所以Go語言也允許我們使用隱式間接引用,直接寫 p.X 就可以:

package main

import "fmt"

type Point struct {
	X int
	Y int
}

func main() {

	v := Point{2, 3}
	var p = &v

	fmt.Println(v) // {2 3}

	(*p).Y = 5
	fmt.Println(v) // {2 5}

	p.Y = 7
	fmt.Println(v) // {2 7}
}

二 類型標識

兩個類型要麼相同要麼不同。

已定義的類型總是與任何其他類型不同。除此之外,如果兩個類型的底層類型字面量在結構上相同,則它們是相同的;換句話說,它們有相同的字面結構,並且相關組件也是一樣的類型,具體細節如下:

  • 如果兩個數組類型具有相同的元素類型和數組長度,則它們相同。
  • 如果兩個切片類型具有相同的元素類型,則它們相同。
  • 如果兩個結構類型具有相同的字段序列,並且相關字段具有相同的名稱、類型和標記,則它們相同。但未導出的字段名如果來自不同的包,則也是不同的。
  • 如果兩個指針類型具有相同的基礎類型,則它們相同。
  • 如果兩個函數類型具有相同的參數及結果值,並且參數及結果值的類型也是相同的,並且兩個函數要麼全是可變參數,要麼都不是,則它們相同。參數及結果名稱可以不一樣。
  • 如果兩個接口類型具有相同的方法集,且方法集的名稱和函數類型一樣,則它們一樣。但未導出的方法名如果來自不同的包,則也是不同的。方法的出現順序無所謂。
  • 如果兩個映射類型具有相同的keyvalue類型,則它們相同。
  • 如果兩個信道類型具有相同的元素類型和方向,則它們相同。

給定以下聲明:

type (
	A0 = []string
	A1 = A0
	A2 = struct{ a, b int }
	A3 = int
	A4 = func(A3, float64) *A0
	A5 = func(x int, _ float64) *[]string
)

type (
	B0 A0
	B1 []string
	B2 struct{ a, b int }
	B3 struct{ a, c int }
	B4 func(int, float64) *B0
	B5 func(x int, y float64) *A1
)

type	C0 = B0

其中這些聲明是相同的:

A0, A1, 和 []string
A2 和 struct{ a, b int }
A3 和 int
A4, func(int, float64) *[]string, 和 A5

B0 和 C0
[]int 和 []int
struct{ a, b *T5 } 和 struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), 和 A5

B0B1不同是因爲它們是由不同的類型定義創建的新類型;func(int, float64) *B0func(x int, y float64) *[]string不同是因爲B0[]string不同。

參考:
https://golang.org/ref/spec#Type_declarations
https://golang.org/ref/spec#Type_identity
https://golang.org/ref/spec#Struct_types
https://golang.org/ref/spec#Composite_literals

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章