Go語言 結(jié)構(gòu)體

2023-02-16 17:37 更新

和C語言類似,Go也支持結(jié)構(gòu)體類型。此篇文章將介紹Go中的結(jié)構(gòu)體類型和結(jié)構(gòu)體值做一個詳細的解釋。

結(jié)構(gòu)體類型和結(jié)構(gòu)體字面量表示形式

每個無名結(jié)構(gòu)體類型的字面形式均由struct關鍵字開頭,后面跟著用一對大括號{},其中包裹著的一系列字段(field)聲明。 一般來說,每個字段聲明由一個字段名和字段類型組成。一個結(jié)構(gòu)體類型的字段數(shù)目可以為0。下面是一個無名結(jié)構(gòu)體類型的字面形式:

struct {
	title  string
	author string
	pages  int
}

上面這個結(jié)構(gòu)體類型含有三個字段。前兩個字段(?title?和?author?)的類型均為?string?。 最后一個字段?pages?的類型為?int?。

有時字段也稱為成員變量。

相鄰的同類型字段可以聲明在一起。比如上面這個類型也可表示成下面這樣:

struct {
	title, author string
	pages         int
}

一個結(jié)構(gòu)體類型的尺寸為它的所有字段的(類型)尺寸之和加上一些填充字節(jié)的數(shù)目。 常常地,編譯器(和運行時)會在一個結(jié)構(gòu)體值的兩個相鄰字段之間填充一些字節(jié)來保證一些字段的地址總是某個整數(shù)的倍數(shù)。 我們可以在后面的內(nèi)存布局一文中了解到字節(jié)填充(padding)和內(nèi)存地址對齊(memory address alignment)。

一個零字段結(jié)構(gòu)體的尺寸為零。

每個結(jié)構(gòu)體字段在它的聲明中可以被指定一個標簽(tag)。從語法上講,字段標簽可以是任意字符串,它們是可選的,默認為空字符串。 但在實踐中,它們應該被表示成用空格分隔的鍵值對形式,并且每個標簽盡量使用直白字面形式(`...`)表示,而鍵值對中的值使用解釋型字面形式("...")表示。 比如下例:

struct {
	Title  string `json:"title" myfmt:"s1"`
	Author string `json:"author,omitempty" myfmt:"s2"`
	Pages  int    `json:"pages,omitempty" myfmt:"n1"`
	X, Y   bool   `myfmt:"b1"`
}

注意:上例中的?X?和?Y?字段的標簽是一樣的(盡管在實踐中基本上從不會這樣使用字段標簽)。

我們可以使用反射來檢視字段的標簽信息。

每個字段標簽的目的取決于具體應用。上面這個例子中的字段標簽用來幫助?encoding/json?標準庫包來將上面這個結(jié)構(gòu)體類型的某個值編碼成JSON數(shù)據(jù)或者從一份JSON數(shù)據(jù)解碼到上面這個結(jié)構(gòu)體類型的某個值中。在編碼和解碼過程中,?encoding/json?標準庫包中的函數(shù)將只考慮導出的結(jié)構(gòu)體字段。這是為什么上面這個結(jié)構(gòu)體的字段均為導出的。

把字段標簽當成字段注釋來使用不是一個好主意。

和C語言不一樣,Go結(jié)構(gòu)體不支持字段聯(lián)合(union)。

上面的例子中展示的結(jié)構(gòu)體類型都是無名的。在實踐中,具名結(jié)構(gòu)體類型用得更流行。

只有導出字段可以被使用在其它代碼包中。非導出字段類以于很多其它語言中的私有或者保護型的成員變量。

一個結(jié)構(gòu)體類型中的字段標簽和字段的聲明順序?qū)Υ私Y(jié)構(gòu)體類型的身份識別很重要。 如果兩個無名結(jié)構(gòu)體類型的各個對應字段聲明都相同(按照它們的出現(xiàn)順序),則此兩個無名結(jié)構(gòu)體類型是等同的。 兩個字段聲明只有在它們的名稱、類型和標簽都等同的情況下才相同。 注意:兩個聲明在不同的代碼包中的非導出字段將總被認為是不同的字段。

一個結(jié)構(gòu)體類型不能(直接或者間接)含有一個類型為此結(jié)構(gòu)類型的字段。

結(jié)構(gòu)體字面量表示形式和結(jié)構(gòu)體值的使用

在Go中,語法形式?T{...}?稱為一個組合字面量形式(composite literal),其中?T?必須為一個類型名或者類型字面形式。 組合字面量形式可以用來表示結(jié)構(gòu)體類型和內(nèi)置容器類型(將在后面的文章中介紹)的值。

注意:組合字面量?T{...}?是一個類型確定值,它的類型為?T?。

假設S是一個結(jié)構(gòu)體類型并且它的底層類型為struct{x int; y bool},S的零值可以表示成下面所示的組合字面量兩種變種形式:

  1. ?S{0, false}?。在此變種形式中,所有的字段名稱均不出現(xiàn),但每個字段的值必須指定,并且每個字段的出現(xiàn)順序和它們的聲明順序必須一致。
  2. ?S{x: 0, y: false}?、?S{y: false, x: 0}?、?S{x: 0}?、?S{y: false}?和?S{}?。 在此變種形式中,字段的名稱和值必須成對出現(xiàn),但是每個字段都不是必須出現(xiàn)的,并且字段的出現(xiàn)順序并不重要。 沒有出現(xiàn)的字段的值被編譯器認為是它們各自類型的零值。?S{}?是最常用的類型?S?的零值的表示形式。

如果?S?是聲明在另一個代碼包中的一個結(jié)構(gòu)體類型,則推薦使用上面所示的第二種變種形式來表示它的值。 因為另一個代碼包的維護者今后可能會在此結(jié)構(gòu)體中添加新的字段,從而導致當前使用的第一種變種形式在今后可能編譯不通過。

當然,上面所示的結(jié)構(gòu)體值的組合字面量也可以用來表示結(jié)構(gòu)體類型的非零值。

對于類型?S?的一個值?v?,我們可以用?v.x?和?v.y?來表示它的字段。 ?v.x?(或?v.y?)這種形式稱為一個選擇器(selector)。其中的?v?稱為此選擇器的屬主。 今后,我們稱一個選擇器中的句點?.?為屬性選擇操作符。

一個例子:

package main

import (
	"fmt"
)

type Book struct {
	title, author string
	pages         int
}

func main() {
	book := Book{"Go語言101", "老貘", 256}
	fmt.Println(book) // {Go語言101 老貘 256}

	// 使用帶字段名的組合字面量來表示結(jié)構(gòu)體值。
	book = Book{author: "老貘", pages: 256, title: "Go語言101"}
	// title和author字段的值都為空字符串"",pages字段的值為0。
	book = Book{}
	// title字段空字符串"",pages字段為0。
	book = Book{author: "老貘"}

	// 使用選擇器來訪問和修改字段值。
	var book2 Book // <=> book2 := Book{}
	book2.author = "Tapir"
	book2.pages = 300
	fmt.Println(book2.pages) // 300
}

如果一個組合字面量中最后一項和結(jié)尾的}處于同一行,則此項后的逗號,是可選的;否則此逗號不可省略。 我們可以閱讀后面的Go代碼斷行規(guī)則一文了解更多斷行規(guī)則。

var _ = Book {
	author: "老貘",
	pages: 256,
	title: "Go語言101", // 這里行尾的逗號不可省略
}

// 下行}前的逗號可以省略。
var _ = Book{author: "老貘", pages: 256, title: "Go語言101",}

關于結(jié)構(gòu)體值的賦值

當一個(源)結(jié)構(gòu)體值被賦值給另外一個(目標)結(jié)構(gòu)體值時,其效果和逐個將源結(jié)構(gòu)體值的各個字段賦值給目標結(jié)構(gòu)體值的各個對應字段的效果是一樣的。

func f() {
	book1 := Book{pages: 300}
	book2 := Book{"Go語言101", "老貘", 256}

	book2 = book1
	// 上面這行和下面這三行是等價的。
	book2.title = book1.title
	book2.author = book1.author
	book2.pages = book1.pages
}

如果兩個結(jié)構(gòu)體值的類型不同,則只有在它們的底層類型相同(要考慮字段標簽)并且其中至少有一個結(jié)構(gòu)體值的類型為無名類型時(換句話說,只有它們可以被隱式轉(zhuǎn)換為對方的類型的時候,見下)才可以互相賦值。

結(jié)構(gòu)體字段的可尋址性

如果一個結(jié)構(gòu)體值是可尋址的,則它的字段也是可尋址的;反之,一個不可尋址的結(jié)構(gòu)體值的字段也是不可尋址的。 不可尋址的字段的值是不可更改的。所有的組合字面量都是不可尋址的。

一個例子:

package main

import "fmt"

func main() {
	type Book struct {
		Pages int
	}
	var book = Book{} // 變量值book是可尋址的
	p := &book.Pages
	*p = 123
	fmt.Println(book) // {123}

	// 下面這兩行編譯不通過,因為Book{}是不可尋址的,
	// 繼而Book{}.Pages也是不可尋址的。
	/*
	Book{}.Pages = 123
	p = &Book{}.Pages // <=> p = &(Book{}.Pages)
	*/
}

注意:選擇器中的屬性選擇操作符?.?的優(yōu)先級比取地址操作符?&?的優(yōu)先級要高。

組合字面量不可尋址但可被取地址

一般來說,只有可被尋址的值才能被取地址,但是Go中有一個語法糖(語法例外):雖然所有的組合字面量都是不可尋址的,但是它們都可被取地址。

例子:

package main

func main() {
	type Book struct {
		Pages int
	}
	// Book{100}是不可尋址的,但是它可以被取地址。
	p := &Book{100} // <=> tmp := Book{100}; p := &tmp
	p.Pages = 200
}

在字段選擇器中,屬主結(jié)構(gòu)體值可以是指針,它將被隱式解引用

比如,在下面的例子中,為了簡潔,(*bookN).pages可以被寫成bookN.pages。 換句話說,在這種簡寫形式中,bookN將被隱式解引用。

package main

func main() {
	type Book struct {
		pages int
	}
	book1 := &Book{100} // book1是一個指針
	book2 := new(Book)  // book2是另外一個指針
	// 像使用結(jié)構(gòu)值一樣來使用結(jié)構(gòu)體值的指針。
	book2.pages = book1.pages
	// 上一行等價于下一行。換句話說,上一行
	// 兩個選擇器中的指針屬主將被自動解引用。
	(*book2).pages = (*book1).pages
}

關于結(jié)構(gòu)體值的比較

如果一個結(jié)構(gòu)體類型是可比較的,則它肯定不包含不可比較類型的字段(這里不忽略名為空標識符_的字段)。

和結(jié)構(gòu)體值的賦值規(guī)則類似,如果兩個不同類型的結(jié)構(gòu)體值均為可比較的,則它們僅在它們的底層類型相同(要考慮字段標簽)并且其中至少有一個結(jié)構(gòu)體值的類型為無名類型時(換句話說,只有它們可以被隱式轉(zhuǎn)換為對方的類型的時候,見下)才可以互相比較。

如果兩個結(jié)構(gòu)體值可以相互比較,則它們的比較結(jié)果等同于逐個比較它們的相應字段(按照字段在代碼中的聲明順序)。 兩個結(jié)構(gòu)體值只有在它們的相應字段都相等的情況下才相等;當一對字段被發(fā)現(xiàn)不相等的或者在比較中產(chǎn)生恐慌的時候,對結(jié)構(gòu)體的比較將提前結(jié)束結(jié)束。 在比較中,名為空標識符_的字段將被忽略掉。

關于結(jié)構(gòu)體值的類型轉(zhuǎn)換

兩個類型分別為?S1?和?S2?的結(jié)構(gòu)體值只有在?S1?和?S2?的底層類型相同(忽略掉字段標簽)的情況下才能相互轉(zhuǎn)換為對方的類型。 特別地,如果?S1?和?S2?的底層類型相同(要考慮字段標簽)并且只要它們其中有一個為無名類型,則此轉(zhuǎn)換可以是隱式的。

比如,對于下面的代碼片段中所示的五個結(jié)構(gòu)體類型:S0、S1、S2S3S4

  • 類型?S0?的值不能被轉(zhuǎn)換為其它四個類型中的任意一個,原因是它與另外四個類型的對應字段名不同(因此底層類型不同)。
  • 類型?S1?、?S2?、?S3?和?S4?的任意兩個值可以轉(zhuǎn)換為對方的類型。

特別地,

  • ?S2?表示的類型的值可以被隱式轉(zhuǎn)化為類型?S3?,反之亦然。
  • ?S2?表示的類型的值可以被隱式轉(zhuǎn)換為類型?S4?,反之亦然。

但是,

  • ?S2?表示的類型的值必須被顯式轉(zhuǎn)換為類型?S1?,反之亦然。
  • 類型?S3?的值必須被顯式轉(zhuǎn)換為類型?S4?,反之亦然。
package main

type S0 struct {
	y int "foo"
	x bool
}

type S1 = struct { // S1是一個無名類型
	x int "foo"
	y bool
}

type S2 = struct { // S2也是一個無名類型
	x int "bar"
	y bool
}

type S3 S2 // S3是一個定義類型(因而具名)。
type S4 S3 // S4是一個定義類型(因而具名)。
// 如果不考慮字段標簽,S3(S4)和S1的底層類型一樣。
// 如果考慮字段標簽,S3(S4)和S1的底層類型不一樣。

var v0, v1, v2, v3, v4 = S0{}, S1{}, S2{}, S3{}, S4{}
func f() {
	v1 = S1(v2); v2 = S2(v1)
	v1 = S1(v3); v3 = S3(v1)
	v1 = S1(v4); v4 = S4(v1)
	v2 = v3; v3 = v2 // 這兩個轉(zhuǎn)換可以是隱式的
	v2 = v4; v4 = v2 // 這兩個轉(zhuǎn)換也可以是隱式的
	v3 = S3(v4); v4 = S4(v3)
}

事實上,兩個結(jié)構(gòu)體值只有在它們可以相互隱式轉(zhuǎn)換為對方的類型的時候才能相互賦值和比較。

匿名結(jié)構(gòu)體類型可以使用在結(jié)構(gòu)體字段聲明中

匿名結(jié)構(gòu)體類型允許出現(xiàn)在結(jié)構(gòu)體字段聲明中。匿名結(jié)構(gòu)體類型也允許出現(xiàn)在組合字面量中。

一個例子:

var aBook = struct {
	author struct { // 此字段的類型為一個匿名結(jié)構(gòu)體類型
		firstName, lastName string
		gender              bool
	}
	title string
	pages int
}{
	author: struct {
		firstName, lastName string
		gender              bool
	}{
		firstName: "Mark",
		lastName: "Twain",
	}, // 此組合字面量中的類型為一個匿名結(jié)構(gòu)體類型
	title: "The Million Pound Note",
	pages: 96,
}

通常來說,為了代碼可讀性,最好少使用匿名結(jié)構(gòu)體類型。

更多關于結(jié)構(gòu)體類型

Go中有一些和結(jié)構(gòu)體類型相關的進階知識點。這些知識點將后面的類型內(nèi)嵌內(nèi)存布局兩篇文章中介紹。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號