Golang 學習筆記四 結構體

一、結構體

《快學 Go 語言》第 8 課 —— 結構體
1.結構體類型的定義
結構體和其它高級語言里的「類」比較類似。下面我們使用結構體語法來定義一個「圓」型

type Circle struct {
  x int
  y int
  Radius int
}

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

2.創建

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

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

可以只指定部分字段的初值,甚至可以一個字段都不指定,那些沒有指定初值的字段會自動初始化為相應類型的「零值」。

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}

結構體的第二種創建形式是不指定字段名稱來順序字段初始化,需要顯示提供所有字段的初值,一個都不能少。這種形式稱之為「順序形式」。var c Circle = Circle {100, 100, 50}

結構體變量創建的第三種形式,使用全局的 new() 函數來創建一個「零值」結構體,所有的字段都被初始化為相應類型的零值。var c *Circle = new(Circle)注意 new() 函數返回的是指針類型。

第四種創建形式,這種形式也是零值初始化,就數它看起來最不雅觀。var c Circle

3.零值結構體和 nil 結構體
nil 結構體是指結構體指針變量沒有指向一個實際存在的內存。這樣的指針變量只會占用 1 個指針的存儲空間,也就是一個機器字的內存大小。

var c *Circle = nil

而零值結構體是會實實在在占用內存空間的,只不過每個字段都是零值。如果結構體里面字段非常多,那么這個內存空間占用肯定也會很大。

4.結構體的拷貝

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.無處不在的結構體
通過觀察 Go 語言的底層源碼,可以發現所有的 Go 語言內置的高級數據結構都是由結構體來完成的。

切片頭的結構體形式如下,它在 64 位機器上將會占用 24 個字節

type slice struct {
  array unsafe.Pointer  // 底層數組的地址
  len int // 長度
  cap int // 容量
}

字符串頭的結構體形式,它在 64 位機器上將會占用 16 個字節

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

字典頭的結構體形式

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

6.結構體的參數傳遞
函數調用時參數傳遞結構體變量,Go 語言支持值傳遞,也支持指針傳遞。值傳遞涉及到結構體字段的淺拷貝,指針傳遞會共享結構體內容,只會拷貝指針地址,規則上和賦值是等價的。下面我們使用兩種傳參方式來編寫擴大圓半徑的函數。

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}

從上面的輸出中可以看到通過值傳遞,在函數里面修改結構體的狀態不會影響到原有結構體的狀態,函數內部的邏輯并沒有產生任何效果。通過指針傳遞就不一樣。

7.結構體方法
Go 語言不是面向對象的語言,它里面不存在類的概念,結構體正是類的替代品。類可以附加很多成員方法,結構體也可以。

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)
}

// 周長
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())
 // 指針變量調用方法形式上是一樣的
 var pc = &c
 fmt.Println(pc.Area(), pc.Circumference())
}

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

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

8.關于GO如何實現面對對象的繼承、多態,是個有趣的話題。參考go是面向對象語言嗎?

9.創建遞歸的數據結構
《go語言圣經》P145
一個命名為S的結構體類型將不能再包含S類型的成員:因為一個聚合的值不能包含它自身。(該限制同樣適應于數組。)但是S類型的結構體可以包含 *S 指針類型的成員,這可以讓我們創建遞歸的數據結構,比如鏈表和樹結構等。在下面的代碼中,我們使用一個二叉樹來實現一個插入排序:

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.結構體的比較
《go語言圣經》P147
如果結構體的全部成員都是可以比較的,那么結構體也是可以比較的,那樣的話兩個結構體將可以使用==或!=運算符進行比較。相等比較運算符==將比較兩個結構體的每個成員,因此下面兩個比較的表達式是等價的:

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.匿名結構體
《go語言圣經》P149

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

這樣改動之后結構體類型變的清晰了,但是這種修改同時也導致了訪問每個成員變得繁瑣:

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

Go語言有一個特性讓我們只聲明一個成員對應的數據類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數據類型必須是命名的類型或指向一個命名的類型的指針。下面的代碼中,Circle和Wheel各自都有一個匿名成員。我們可以說Point類型被嵌入到了Circle結構體,同時Circle類型被嵌入到了Wheel結構體。

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

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

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

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

不幸的是,結構體字面值并沒有簡短表示匿名成員的語法, 因此下面的語句都不能編譯通過:

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

結構體字面值必須遵循形狀類型聲明時的結構,所以我們只能用下面的兩種語法,它們彼此是等價的:

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函數中%v參數包含的#副詞,它表示用和Go語言類似的語法打印值。對于結構體類型來說,將包含每個成員的名字。

因為匿名成員也有一個隱式的名字,因此不能同時包含兩個類型相同的匿名成員,這會導致名字沖突。同時,因為成員的名字是由其類型隱式地決定的,所有匿名成員也有可見性的規則約束。在上面的例子中,Point和Circle匿名成員都是導出的。即使它們不導出(比如改成小寫字母開頭的point和circle),我們依然可以用簡短形式訪問匿名成員嵌套的成員

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

但是在包外部,因為circle和point沒有導出不能訪問它們的成員,因此簡短的匿名成員訪問語法也是禁止的。

到目前為止,我們看到匿名成員特性只是對訪問嵌套成員的點運算符提供了簡短的語法糖。稍后,我們將會看到匿名成員并不要求是結構體類型;其實任何命名的類型都可以作為結構體的匿名成員。但是為什么要嵌入一個沒有任何子成員類型的匿名成員類型呢?答案是匿名類型的方法集。簡短的點運算符語法可以用于選擇匿名成員嵌套的成員,也可以用于訪問它們的方法。實際上,外層的結構體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導出的全部的方法。這個機制可以用于將一個有簡單行為的對象組合成有復雜行為的對象。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容