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

2023-02-16 17:37 更新

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

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

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

struct {
	title  string
	author string
	pages  int
}

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

有時(shí)字段也稱為成員變量。

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

struct {
	title, author string
	pages         int
}

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

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

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

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?字段的標(biāo)簽是一樣的(盡管在實(shí)踐中基本上從不會(huì)這樣使用字段標(biāo)簽)。

我們可以使用反射來(lái)檢視字段的標(biāo)簽信息。

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

把字段標(biāo)簽當(dāng)成字段注釋來(lái)使用不是一個(gè)好主意。

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

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

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

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

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

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

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

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

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

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

如果?S?是聲明在另一個(gè)代碼包中的一個(gè)結(jié)構(gòu)體類型,則推薦使用上面所示的第二種變種形式來(lái)表示它的值。 因?yàn)榱硪粋€(gè)代碼包的維護(hù)者今后可能會(huì)在此結(jié)構(gòu)體中添加新的字段,從而導(dǎo)致當(dāng)前使用的第一種變種形式在今后可能編譯不通過(guò)。

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

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

一個(gè)例子:

package main

import (
	"fmt"
)

type Book struct {
	title, author string
	pages         int
}

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

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

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

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

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

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

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

當(dāng)一個(gè)(源)結(jié)構(gòu)體值被賦值給另外一個(gè)(目標(biāo))結(jié)構(gòu)體值時(shí),其效果和逐個(gè)將源結(jié)構(gòu)體值的各個(gè)字段賦值給目標(biāo)結(jié)構(gòu)體值的各個(gè)對(duì)應(yīng)字段的效果是一樣的。

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

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

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

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

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

一個(gè)例子:

package main

import "fmt"

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

	// 下面這兩行編譯不通過(guò),因?yàn)锽ook{}是不可尋址的,
	// 繼而B(niǎo)ook{}.Pages也是不可尋址的。
	/*
	Book{}.Pages = 123
	p = &Book{}.Pages // <=> p = &(Book{}.Pages)
	*/
}

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

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

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

例子:

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)體值可以是指針,它將被隱式解引用

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

package main

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

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

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

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

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

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

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

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

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

特別地,

  • ?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是一個(gè)無(wú)名類型
	x int "foo"
	y bool
}

type S2 = struct { // S2也是一個(gè)無(wú)名類型
	x int "bar"
	y bool
}

type S3 S2 // S3是一個(gè)定義類型(因而具名)。
type S4 S3 // S4是一個(gè)定義類型(因而具名)。
// 如果不考慮字段標(biāo)簽,S3(S4)和S1的底層類型一樣。
// 如果考慮字段標(biāo)簽,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 // 這兩個(gè)轉(zhuǎn)換可以是隱式的
	v2 = v4; v4 = v2 // 這兩個(gè)轉(zhuǎn)換也可以是隱式的
	v3 = S3(v4); v4 = S4(v3)
}

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

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

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

一個(gè)例子:

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)