鍍金池/ 教程/ GO/ 10.1 結(jié)構(gòu)體定義
4.7 strings 和 strconv 包
13.6 啟動外部命令和程序
?# 11.4 類型判斷:type-switch
12.1 讀取用戶的輸入
10.6 方法
12.2 文件讀寫
13 錯誤處理與測試
9.3 鎖和 sync 包
12.3 文件拷貝
?# 11.7 第一個例子:使用 Sorter 接口排序
?# 11.5 測試一個值是否實(shí)現(xiàn)了某個接口
6.4 defer 和追蹤
12.10 XML 數(shù)據(jù)格式
13.10 性能調(diào)試:分析并優(yōu)化 Go 程序
?# 11.1 接口是什么
2.2 Go 環(huán)境變量
2.6 安裝目錄清單
2.5 在 Windows 上安裝 Go
11.11 Printf 和反射
1.2 語言的主要特性與發(fā)展的環(huán)境和影響因素
9.0 包(package)
7.4 切片重組(reslice)
13.2 運(yùn)行時異常和 panic
10.2 使用工廠方法創(chuàng)建結(jié)構(gòu)體實(shí)例
12.8 使用接口的實(shí)際例子:fmt.Fprintf
2.4 在 Mac OS X 上安裝 Go
3.8 Go 性能說明
7.2 切片
8.0 Map
3.1 Go 開發(fā)環(huán)境的基本要求
5.6 標(biāo)簽與 goto
6.10 使用閉包調(diào)試
9.5 自定義包和可見性
4.3 常量
?# 11.2 接口嵌套接口
6.5 內(nèi)置函數(shù)
前言
10.8 垃圾回收和 SetFinalizer
2.8 Go 解釋器
13.7 Go 中的單元測試和基準(zhǔn)測試
6.8 閉包
4.9 指針
13.1 錯誤處理
10.1 結(jié)構(gòu)體定義
5.1 if-else 結(jié)構(gòu)
6.6 遞歸函數(shù)
9.9 通過 Git 打包和安裝
2.7 Go 運(yùn)行時(runtime)
10.7 類型的 String() 方法和格式化描述符
3.7 其它工具
9.6 為自定義包使用 godoc
11.12 接口與動態(tài)類型
13.3 從 panic 中恢復(fù)(Recover)
10.3 使用自定義包中的結(jié)構(gòu)體
11.14 結(jié)構(gòu)體、集合和高階函數(shù)
3.6 生成代碼文檔
9.2 regexp 包
4.1 文件名、關(guān)鍵字與標(biāo)識符
?# 11.6 使用方法集與接口
7.0 數(shù)組與切片
7.1 聲明和初始化
12.11 用 Gob 傳輸數(shù)據(jù)
5.5 Break 與 continue
1.1 起源與發(fā)展
?# 11 接口(Interfaces)與反射(reflection)
6.9 應(yīng)用閉包:將函數(shù)作為返回值
4.2 Go 程序的基本結(jié)構(gòu)和要素
8.6 將 map 的鍵值對調(diào)
6.11 計算函數(shù)執(zhí)行時間
5.0 控制結(jié)構(gòu)
10.5 匿名字段和內(nèi)嵌結(jié)構(gòu)體
4.6 字符串
3.0 編輯器、集成開發(fā)環(huán)境與其它工具
13.8 測試的具體例子
7.6 字符串、數(shù)組和切片的應(yīng)用
8.4 map 類型的切片
3.9 與其它語言進(jìn)行交互
7.3 For-range 結(jié)構(gòu)
9.7 使用 go install 安裝自定義包
6.0 函數(shù)
9.8 自定義包的目錄結(jié)構(gòu)、go install 和 go test
6.3 傳遞變長參數(shù)
13.9 用(測試數(shù)據(jù))表驅(qū)動測試
11.9 空接口
8.1 聲明、初始化和 make
6.2 函數(shù)參數(shù)與返回值
9.11 在 Go 程序中使用外部庫
3.3 調(diào)試器
4.5 基本類型和運(yùn)算符
?# 11.8 第二個例子:讀和寫
12.5 用 buffer 讀取文件
總結(jié):Go 中的面向?qū)ο?/span>
11.10 反射包
12.7 用 defer 關(guān)閉文件
9.4 精密計算和 big 包
4.4 變量
6.1 介紹
13.4 自定義包中的錯誤處理和 panicking
12.4 從命令行讀取參數(shù)
9.10 Go 的外部包和項(xiàng)目
8.3 for-range 的配套用法
3.5 格式化代碼
10.4 帶標(biāo)簽的結(jié)構(gòu)體
7.5 切片的復(fù)制與追加
?# 11.3 類型斷言:如何檢測和轉(zhuǎn)換接口變量的類型
5.4 for 結(jié)構(gòu)
4.8 時間和日期
2.3 在 Linux 上安裝 Go
12 讀寫數(shù)據(jù)
6.12 通過內(nèi)存緩存來提升性能
9.1 標(biāo)準(zhǔn)庫概述
12.6 用切片讀寫文件
10 結(jié)構(gòu)(struct)與方法(method)
8.5 map 的排序
12.9 JSON 數(shù)據(jù)格式
13.5 一種用閉包處理錯誤的模式
3.2 編輯器和集成開發(fā)環(huán)境
12.12 Go 中的密碼學(xué)
5.2 測試多返回值函數(shù)的錯誤
6.7 將函數(shù)作為參數(shù)
8.2 測試鍵值對是否存在及刪除元素
3.4 構(gòu)建并運(yùn)行 Go 程序
2.1 平臺與架構(gòu)
5.3 switch 結(jié)構(gòu)

10.1 結(jié)構(gòu)體定義

結(jié)構(gòu)體定義的一般方式如下:

type identifier struct {
    field1 type1
    field2 type2
    ...
}

type T struct {a, b int} 也是合法的語法,它更適用于簡單的結(jié)構(gòu)體。

結(jié)構(gòu)體里的字段都有 名字,像 field1、field2 等,如果字段在代碼中從來也不會被用到,那么可以命名它為 _

結(jié)構(gòu)體的字段可以是任何類型,甚至是結(jié)構(gòu)體本身(參考第 10.5 節(jié)),也可以是函數(shù)或者接口(參考第 11 章)??梢月暶鹘Y(jié)構(gòu)體類型的一個變量,然后像下面這樣給它的字段賦值:

var s T
s.a = 5
s.b = 8

數(shù)組可以看作是一種結(jié)構(gòu)體類型,不過它使用下標(biāo)而不是具名的字段。

使用 new

使用 new 函數(shù)給一個新的結(jié)構(gòu)體變量分配內(nèi)存,它返回指向已分配內(nèi)存的指針:var t *T = new(T),如果需要可以把這條語句放在不同的行(比如定義是包范圍的,但是分配卻沒有必要在開始就做)。

var t *T
t = new(T)

寫這條語句的慣用方法是:t := new(T),變量 t 是一個指向 T的指針,此時結(jié)構(gòu)體字段的值是它們所屬類型的零值。

聲明 var t T 也會給 t 分配內(nèi)存,并零值化內(nèi)存,但是這個時候 t 是類型T。在這兩種方式中,t 通常被稱做類型 T 的一個實(shí)例(instance)或?qū)ο螅╫bject)。

示例 10.1 structs_fields.go 給出了一個非常簡單的例子:

package main
import "fmt"

type struct1 struct {
    i1  int
    f1  float32
    str string
}

func main() {
    ms := new(struct1)
    ms.i1 = 10
    ms.f1 = 15.5
    ms.str= "Chris"

    fmt.Printf("The int is: %d\n", ms.i1)
    fmt.Printf("The float is: %f\n", ms.f1)
    fmt.Printf("The string is: %s\n", ms.str)
    fmt.Println(ms)
}

輸出:

The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}

使用 fmt.Println 打印一個結(jié)構(gòu)體的默認(rèn)輸出可以很好的顯示它的內(nèi)容,類似使用 %v 選項(xiàng)。

就像在面向?qū)ο笳Z言所作的那樣,可以使用點(diǎn)號符給字段賦值:structname.fieldname = value。

同樣的,使用點(diǎn)號符可以獲取結(jié)構(gòu)體字段的值:structname.fieldname。

在 Go 語言中這叫 選擇器(selector)。無論變量是一個結(jié)構(gòu)體類型還是一個結(jié)構(gòu)體類型指針,都使用同樣的 選擇器符(selector-notation) 來引用結(jié)構(gòu)體的字段:

type myStruct struct { i int }
var v myStruct    // v是結(jié)構(gòu)體類型變量
var p *myStruct   // p是指向一個結(jié)構(gòu)體類型變量的指針
v.i
p.i

初始化一個結(jié)構(gòu)體實(shí)例(一個結(jié)構(gòu)體字面量:struct-literal)的更簡短和慣用的方式如下:

    ms := &struct1{10, 15.5, "Chris"}
    // 此時ms的類型是 *struct1

或者:

    var ms struct1
    ms = struct1{10, 15.5, "Chris"}

混合字面量語法(composite literal syntax)&struct1{a, b, c} 是一種簡寫,底層仍然會調(diào)用 new (),這里值的順序必須按照字段順序來寫。在下面的例子中能看到可以通過在值的前面放上字段名來初始化字段的方式。表達(dá)式 new(Type)&Type{} 是等價的。

時間間隔(開始和結(jié)束時間以秒為單位)是使用結(jié)構(gòu)體的一個典型例子:

type Interval struct {
    start int
    end   int
}

初始化方式:

intr := Interval{0, 3}            (A)
intr := Interval{end:5, start:1}  (B)
intr := Interval{end:5}           (C)

在(A)中,值必須以字段在結(jié)構(gòu)體定義時的順序給出,& 不是必須的。(B)顯示了另一種方式,字段名加一個冒號放在值的前面,這種情況下值的順序不必一致,并且某些字段還可以被忽略掉,就像(C)中那樣。

結(jié)構(gòu)體類型和字段的命名遵循可見性規(guī)則(第 4.2 節(jié)),一個導(dǎo)出的結(jié)構(gòu)體類型中有些字段是導(dǎo)出的,另一些不是,這是可能的。

下圖說明了結(jié)構(gòu)體類型實(shí)例和一個指向它的指針的內(nèi)存布局:

type Point struct { x, y int }

使用 new 初始化:

作為結(jié)構(gòu)體字面量初始化:

類型 strcut1 在定義它的包 pack1 中必須是唯一的,它的完全類型名是:pack1.struct1

下面的例子 Listing 10.2—person.go 顯示了一個結(jié)構(gòu)體 Person,一個方法,方法有一個類型為 *Person 的參數(shù)(因此對象本身是可以被改變的),以及三種調(diào)用這個方法的不同方式:

package main
import (
    "fmt"
    "strings"
)

type Person struct {
    firstName   string
    lastName    string
}

func upPerson(p *Person) {
    p.firstName = strings.ToUpper(p.firstName)
    p.lastName = strings.ToUpper(p.lastName)
}

func main() {
    // 1-struct as a value type:
    var pers1 Person
    pers1.firstName = "Chris"
    pers1.lastName = "Woodward"
    upPerson(&pers1)
    fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)

    // 2—struct as a pointer:
    pers2 := new(Person)
    pers2.firstName = "Chris"
    pers2.lastName = "Woodward"
    (*pers2).lastName = "Woodward"  // 這是合法的
    upPerson(pers2)
    fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)

    // 3—struct as a literal:
    pers3 := &Person{"Chris","Woodward"}
    upPerson(pers3)
    fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}

輸出:

The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD

在上面例子的第二種情況中,可以直接通過指針,像 pers2.lastName="Woodward" 這樣給結(jié)構(gòu)體字段賦值,沒有像 C++ 中那樣需要使用 -> 操作符,Go 會自動做這樣的轉(zhuǎn)換。

注意也可以通過解指針的方式來設(shè)置值:(*pers2).lastName = "Woodward"

結(jié)構(gòu)體的內(nèi)存布局

Go 語言中,結(jié)構(gòu)體和它所包含的數(shù)據(jù)在內(nèi)存中是以連續(xù)塊的形式存在的,即使結(jié)構(gòu)體中嵌套有其他的結(jié)構(gòu)體,這在性能上帶來了很大的優(yōu)勢。不像 Java 中的引用類型,一個對象和它里面包含的對象可能會在不同的內(nèi)存空間中,這點(diǎn)和 Go 語言中的指針很像。下面的例子清晰地說明了這些情況:

type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }

遞歸結(jié)構(gòu)體

結(jié)構(gòu)體類型可以通過引用自身來定義。這在定義鏈表或二叉樹的元素(通常叫節(jié)點(diǎn))時特別有用,此時節(jié)點(diǎn)包含指向臨近節(jié)點(diǎn)的鏈接(地址)。如下所示,鏈表中的 su,樹中的 rile 分別是指向別的節(jié)點(diǎn)的指針。

鏈表:

這塊的 data 字段用于存放有效數(shù)據(jù)(比如 float64),su 指針指向后繼節(jié)點(diǎn)。

Go 代碼:

type Node struct {
    data    float64
    su      *Node
}

鏈表中的第一個元素叫 head,它指向第二個元素;最后一個元素叫 tail,它沒有后繼元素,所以它的 su 為 nil 值。當(dāng)然真實(shí)的鏈接會有很多數(shù)據(jù)節(jié)點(diǎn),并且鏈表可以動態(tài)增長或收縮。

同樣地可以定義一個雙向鏈表,它有一個前趨節(jié)點(diǎn) pr 和一個后繼節(jié)點(diǎn) su

type Node struct {
    pr      *Node
    data    float64
    su      *Node
}

二叉樹:

二叉樹中每個節(jié)點(diǎn)最多能鏈接至兩個節(jié)點(diǎn):左節(jié)點(diǎn)(le)和右節(jié)點(diǎn)(ri),這兩個節(jié)點(diǎn)本身又可以有左右節(jié)點(diǎn),依次類推。樹的頂層節(jié)點(diǎn)叫根節(jié)點(diǎn)(root),底層沒有子節(jié)點(diǎn)的節(jié)點(diǎn)叫葉子節(jié)點(diǎn)(leaves),葉子節(jié)點(diǎn)的 leri 指針為 nil 值。在 Go 中可以如下定義二叉樹:

type Tree strcut {
    le      *Tree
    data    float64
    ri      *Tree
}

結(jié)構(gòu)體轉(zhuǎn)換

Go 中的類型轉(zhuǎn)換遵循嚴(yán)格的規(guī)則。當(dāng)為結(jié)構(gòu)體定義了一個 alias 類型時,此結(jié)構(gòu)體類型和它的 alias 類型都有相同的底層類型,它們可以如示例 10.3 那樣互相轉(zhuǎn)換,同時需要注意其中非法賦值或轉(zhuǎn)換引起的編譯錯誤。

示例 10.3:

package main
import "fmt"

type number struct {
    f float32
}

type nr number   // alias type

func main() {
    a := number{5.0}
    b := nr{5.0}
    // var i float32 = b   // compile-error: cannot use b (type nr) as type float32 in assignment
    // var i = float32(b)  // compile-error: cannot convert b (type nr) to type float32
    // var c number = b    // compile-error: cannot use b (type nr) as type number in assignment
    // needs a conversion:
    var c = number(b)
    fmt.Println(a, b, c)
}

輸出:

{5} {5} {5}

練習(xí) 10.1 vcard.go:

定義結(jié)構(gòu)體 Address 和 VCard,后者包含一個人的名字、地址編號、出生日期和圖像,試著選擇正確的數(shù)據(jù)類型。構(gòu)建一個自己的 vcard 并打印它的內(nèi)容。

提示:
VCard 必須包含住址,它應(yīng)該以值類型還是以指針類型放在 VCard 中呢?
第二種會好點(diǎn),因?yàn)樗加脙?nèi)存少。包含一個名字和兩個指向地址的指針的 Address 結(jié)構(gòu)體可以使用 %v 打?。?{Kersschot 0x126d2b80 0x126d2be0}

練習(xí) 10.2 persionext1.go:

修改 persionext1.go,使它的參數(shù) upPerson 不是一個指針,解釋下二者的區(qū)別。

練習(xí) 10.3 point.go:

使用坐標(biāo) X、Y 定義一個二維 Point 結(jié)構(gòu)體。同樣地,對一個三維點(diǎn)使用它的極坐標(biāo)定義一個 Polar 結(jié)構(gòu)體。實(shí)現(xiàn)一個 Abs() 方法來計算一個 Point 表示的向量的長度,實(shí)現(xiàn)一個 Scale 方法,它將點(diǎn)的坐標(biāo)乘以一個尺度因子(提示:使用 math 包里的 Sqrt 函數(shù))(function Scale that multiplies the coordinates of a point with a scale factor)。

練習(xí) 10.3 rectangle.go:

定義一個 Rectangle 結(jié)構(gòu)體,它的長和寬是 int 類型,并定義方法 Area()Perimeter(),然后進(jìn)行測試。

鏈接