Golang 學(xué)習(xí)筆記四 結(jié)構(gòu)體

一、結(jié)構(gòu)體

《快學(xué) Go 語(yǔ)言》第 8 課 —— 結(jié)構(gòu)體
1.結(jié)構(gòu)體類型的定義
結(jié)構(gòu)體和其它高級(jí)語(yǔ)言里的「類」比較類似。下面我們使用結(jié)構(gòu)體語(yǔ)法來(lái)定義一個(gè)「圓」型

type Circle struct {
  x int
  y int
  Radius int
}

Circle 結(jié)構(gòu)體內(nèi)部有三個(gè)變量,分別是圓心的坐標(biāo)以及半徑。特別需要注意是結(jié)構(gòu)體內(nèi)部變量的大小寫,首字母大寫是公開(kāi)變量,首字母小寫是內(nèi)部變量,分別相當(dāng)于類成員變量的 Public 和 Private 類別。內(nèi)部變量只有屬于同一個(gè) package(簡(jiǎn)單理解就是同一個(gè)目錄)的代碼才能直接訪問(wèn)。

2.創(chuàng)建

func main() {
    var c Circle = Circle {
        x: 100,
        y: 100,
        Radius: 50,  // 注意這里的逗號(hào)不能少
    }
    fmt.Printf("%+v\n", c)
}

----------
{x:100 y:100 Radius:50}

可以只指定部分字段的初值,甚至可以一個(gè)字段都不指定,那些沒(méi)有指定初值的字段會(huì)自動(dòng)初始化為相應(yīng)類型的「零值」。

func main() {
    var c1 Circle = Circle {
        Radius: 50,
    }
    var c2 Circle = Circle {}
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)
}

----------
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:0}

結(jié)構(gòu)體的第二種創(chuàng)建形式是不指定字段名稱來(lái)順序字段初始化,需要顯示提供所有字段的初值,一個(gè)都不能少。這種形式稱之為「順序形式」。var c Circle = Circle {100, 100, 50}

結(jié)構(gòu)體變量創(chuàng)建的第三種形式,使用全局的 new() 函數(shù)來(lái)創(chuàng)建一個(gè)「零值」結(jié)構(gòu)體,所有的字段都被初始化為相應(yīng)類型的零值。var c *Circle = new(Circle)注意 new() 函數(shù)返回的是指針類型。

第四種創(chuàng)建形式,這種形式也是零值初始化,就數(shù)它看起來(lái)最不雅觀。var c Circle

3.零值結(jié)構(gòu)體和 nil 結(jié)構(gòu)體
nil 結(jié)構(gòu)體是指結(jié)構(gòu)體指針變量沒(méi)有指向一個(gè)實(shí)際存在的內(nèi)存。這樣的指針變量只會(huì)占用 1 個(gè)指針的存儲(chǔ)空間,也就是一個(gè)機(jī)器字的內(nèi)存大小。

var c *Circle = nil

而零值結(jié)構(gòu)體是會(huì)實(shí)實(shí)在在占用內(nèi)存空間的,只不過(guò)每個(gè)字段都是零值。如果結(jié)構(gòu)體里面字段非常多,那么這個(gè)內(nèi)存空間占用肯定也會(huì)很大。

4.結(jié)構(gòu)體的拷貝

func main() {
    var c1 Circle = Circle {Radius: 50}
    var c2 Circle = c1
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)
    c1.Radius = 100
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)

    var c3 *Circle = &Circle {Radius: 50}
    var c4 *Circle = c3
    fmt.Printf("%+v\n", c3)
    fmt.Printf("%+v\n", c4)
    c3.Radius = 100
    fmt.Printf("%+v\n", c3)
    fmt.Printf("%+v\n", c4)
}

---------------
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:100}
{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:100}
&{x:0 y:0 Radius:100}

5.無(wú)處不在的結(jié)構(gòu)體
通過(guò)觀察 Go 語(yǔ)言的底層源碼,可以發(fā)現(xiàn)所有的 Go 語(yǔ)言內(nèi)置的高級(jí)數(shù)據(jù)結(jié)構(gòu)都是由結(jié)構(gòu)體來(lái)完成的。

切片頭的結(jié)構(gòu)體形式如下,它在 64 位機(jī)器上將會(huì)占用 24 個(gè)字節(jié)

type slice struct {
  array unsafe.Pointer  // 底層數(shù)組的地址
  len int // 長(zhǎng)度
  cap int // 容量
}

字符串頭的結(jié)構(gòu)體形式,它在 64 位機(jī)器上將會(huì)占用 16 個(gè)字節(jié)

type string struct {
  array unsafe.Pointer // 底層數(shù)組的地址
  len int
}

字典頭的結(jié)構(gòu)體形式

type hmap struct {
  count int
  ...
  buckets unsafe.Pointer  // hash桶地址
  ...
}

6.結(jié)構(gòu)體的參數(shù)傳遞
函數(shù)調(diào)用時(shí)參數(shù)傳遞結(jié)構(gòu)體變量,Go 語(yǔ)言支持值傳遞,也支持指針傳遞。值傳遞涉及到結(jié)構(gòu)體字段的淺拷貝,指針傳遞會(huì)共享結(jié)構(gòu)體內(nèi)容,只會(huì)拷貝指針地址,規(guī)則上和賦值是等價(jià)的。下面我們使用兩種傳參方式來(lái)編寫擴(kuò)大圓半徑的函數(shù)。

package main

import "fmt"

type Circle struct {
    x int
    y int
    Radius int
}

func expandByValue(c Circle) {
    c.Radius *= 2
}

func expandByPointer(c *Circle) {
    c.Radius *= 2
}

func main() {
    var c = Circle {Radius: 50}
    expandByValue(c)
    fmt.Println(c)
    expandByPointer(&c)
    fmt.Println(c)
}

---------
{0 0 50}
{0 0 100}

從上面的輸出中可以看到通過(guò)值傳遞,在函數(shù)里面修改結(jié)構(gòu)體的狀態(tài)不會(huì)影響到原有結(jié)構(gòu)體的狀態(tài),函數(shù)內(nèi)部的邏輯并沒(méi)有產(chǎn)生任何效果。通過(guò)指針傳遞就不一樣。

7.結(jié)構(gòu)體方法
Go 語(yǔ)言不是面向?qū)ο蟮恼Z(yǔ)言,它里面不存在類的概念,結(jié)構(gòu)體正是類的替代品。類可以附加很多成員方法,結(jié)構(gòu)體也可以。

package main

import "fmt"
import "math"

type Circle struct {
 x int
 y int
 Radius int
}

// 面積
func (c Circle) Area() float64 {
 return math.Pi * float64(c.Radius) * float64(c.Radius)
}

// 周長(zhǎng)
func (c Circle) Circumference() float64 {
 return 2 * math.Pi * float64(c.Radius)
}

func main() {
 var c = Circle {Radius: 50}
 fmt.Println(c.Area(), c.Circumference())
 // 指針變量調(diào)用方法形式上是一樣的
 var pc = &c
 fmt.Println(pc.Area(), pc.Circumference())
}

-----------
7853.981633974483 314.1592653589793
7853.981633974483 314.1592653589793

Go 語(yǔ)言不喜歡類型的隱式轉(zhuǎn)換,所以需要將整形顯示轉(zhuǎn)換成浮點(diǎn)型,不是很好看,不過(guò)這就是 Go 語(yǔ)言的基本規(guī)則,顯式的代碼可能不夠簡(jiǎn)潔,但是易于理解。
Go 語(yǔ)言的結(jié)構(gòu)體方法里面沒(méi)有 self 和 this 這樣的關(guān)鍵字來(lái)指代當(dāng)前的對(duì)象,它是用戶自己定義的變量名稱,通常我們都使用單個(gè)字母來(lái)表示。
Go 語(yǔ)言的方法名稱也分首字母大小寫,它的權(quán)限規(guī)則和字段一樣,首字母大寫就是公開(kāi)方法,首字母小寫就是內(nèi)部方法,只能歸屬于同一個(gè)包的代碼才可以訪問(wèn)內(nèi)部方法。
結(jié)構(gòu)體的值類型和指針類型訪問(wèn)內(nèi)部字段和方法在形式上是一樣的。這點(diǎn)不同于 C++ 語(yǔ)言,在 C++ 語(yǔ)言里,值訪問(wèn)使用句點(diǎn) . 操作符,而指針訪問(wèn)需要使用箭頭 -> 操作符。

8.關(guān)于GO如何實(shí)現(xiàn)面對(duì)對(duì)象的繼承、多態(tài),是個(gè)有趣的話題。參考go是面向?qū)ο笳Z(yǔ)言嗎?

9.創(chuàng)建遞歸的數(shù)據(jù)結(jié)構(gòu)
《go語(yǔ)言圣經(jīng)》P145
一個(gè)命名為S的結(jié)構(gòu)體類型將不能再包含S類型的成員:因?yàn)橐粋€(gè)聚合的值不能包含它自身。(該限制同樣適應(yīng)于數(shù)組。)但是S類型的結(jié)構(gòu)體可以包含 *S 指針類型的成員,這可以讓我們創(chuàng)建遞歸的數(shù)據(jù)結(jié)構(gòu),比如鏈表和樹(shù)結(jié)構(gòu)等。在下面的代碼中,我們使用一個(gè)二叉樹(shù)來(lái)實(shí)現(xiàn)一個(gè)插入排序:

type tree struct {
value int
left, right *tree
}
// Sort sorts values in place.
func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root, v)
}
appendValues(values[:0], root)
}
// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}
func add(t *tree, value int) *tree {
if t == nil {
// Equivalent to return &tree{value: value}.
t = new(tree)
t.value = value
return t
}
if value < t.value {
t.left = add(t.left, value)
} else {
t.right = add(t.right, value)
}
return t
}

10.結(jié)構(gòu)體的比較
《go語(yǔ)言圣經(jīng)》P147
如果結(jié)構(gòu)體的全部成員都是可以比較的,那么結(jié)構(gòu)體也是可以比較的,那樣的話兩個(gè)結(jié)構(gòu)體將可以使用==或!=運(yùn)算符進(jìn)行比較。相等比較運(yùn)算符==將比較兩個(gè)結(jié)構(gòu)體的每個(gè)成員,因此下面兩個(gè)比較的表達(dá)式是等價(jià)的:

type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q) // "false"

11.匿名結(jié)構(gòu)體
《go語(yǔ)言圣經(jīng)》P149

type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}

這樣改動(dòng)之后結(jié)構(gòu)體類型變的清晰了,但是這種修改同時(shí)也導(dǎo)致了訪問(wèn)每個(gè)成員變得繁瑣:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

Go語(yǔ)言有一個(gè)特性讓我們只聲明一個(gè)成員對(duì)應(yīng)的數(shù)據(jù)類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數(shù)據(jù)類型必須是命名的類型或指向一個(gè)命名的類型的指針。下面的代碼中,Circle和Wheel各自都有一個(gè)匿名成員。我們可以說(shuō)Point類型被嵌入到了Circle結(jié)構(gòu)體,同時(shí)Circle類型被嵌入到了Wheel結(jié)構(gòu)體。

type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}

得益于匿名嵌入的特性,我們可以直接訪問(wèn)葉子屬性而不需要給出完整的路徑:

var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20

在右邊的注釋中給出的顯式形式訪問(wèn)這些葉子成員的語(yǔ)法依然有效,因此匿名成員并不是真的無(wú)法訪問(wèn)了。其中匿名成員Circle和Point都有自己的名字——就是命名的類型名字——但是這些名字在點(diǎn)操作符中是可選的。我們?cè)谠L問(wèn)子成員的時(shí)候可以忽略任何匿名成員部分。

不幸的是,結(jié)構(gòu)體字面值并沒(méi)有簡(jiǎn)短表示匿名成員的語(yǔ)法, 因此下面的語(yǔ)句都不能編譯通過(guò):

w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

結(jié)構(gòu)體字面值必須遵循形狀類型聲明時(shí)的結(jié)構(gòu),所以我們只能用下面的兩種語(yǔ)法,它們彼此是等價(jià)的:

gopl.io/ch4/embed
w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
fmt.Printf("%#v\n", w)

Output:Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}需要注意的是Printf函數(shù)中%v參數(shù)包含的#副詞,它表示用和Go語(yǔ)言類似的語(yǔ)法打印值。對(duì)于結(jié)構(gòu)體類型來(lái)說(shuō),將包含每個(gè)成員的名字。

因?yàn)槟涿蓡T也有一個(gè)隱式的名字,因此不能同時(shí)包含兩個(gè)類型相同的匿名成員,這會(huì)導(dǎo)致名字沖突。同時(shí),因?yàn)槌蓡T的名字是由其類型隱式地決定的,所有匿名成員也有可見(jiàn)性的規(guī)則約束。在上面的例子中,Point和Circle匿名成員都是導(dǎo)出的。即使它們不導(dǎo)出(比如改成小寫字母開(kāi)頭的point和circle),我們依然可以用簡(jiǎn)短形式訪問(wèn)匿名成員嵌套的成員

w.X = 8 // equivalent to w.circle.point.X = 8

但是在包外部,因?yàn)閏ircle和point沒(méi)有導(dǎo)出不能訪問(wèn)它們的成員,因此簡(jiǎn)短的匿名成員訪問(wèn)語(yǔ)法也是禁止的。

到目前為止,我們看到匿名成員特性只是對(duì)訪問(wèn)嵌套成員的點(diǎn)運(yùn)算符提供了簡(jiǎn)短的語(yǔ)法糖。稍后,我們將會(huì)看到匿名成員并不要求是結(jié)構(gòu)體類型;其實(shí)任何命名的類型都可以作為結(jié)構(gòu)體的匿名成員。但是為什么要嵌入一個(gè)沒(méi)有任何子成員類型的匿名成員類型呢?答案是匿名類型的方法集。簡(jiǎn)短的點(diǎn)運(yùn)算符語(yǔ)法可以用于選擇匿名成員嵌套的成員,也可以用于訪問(wèn)它們的方法。實(shí)際上,外層的結(jié)構(gòu)體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導(dǎo)出的全部的方法。這個(gè)機(jī)制可以用于將一個(gè)有簡(jiǎn)單行為的對(duì)象組合成有復(fù)雜行為的對(duì)象。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容