本節咱們要開講 Go 語言在數據結構上最重要的概念 —— 結構體。若是說 Go 語言的基礎類型是原子,那麼結構體就是分子。分子是原子的組合,讓形式有限的基礎類型變化出豐富多樣的形態結構。結構體裏面裝的是基礎類型、切片、字典、數組以及其它類型的結構體等等。數組
由於結構體的存在,Go 語言的變量纔有了更加豐富多彩的形式,Go 語言程序的高樓大廈正是經過結構體一層層組裝起來的。數據結構
結構體和其它高級語言裏的「類」比較相似。下面咱們使用結構體語法來定義一個「圓」型app
type Circle struct {
x int
y int
Radius int
}
複製代碼
Circle 結構體內部有三個變量,分別是圓心的座標以及半徑。特別須要注意是結構體內部變量的大小寫,首字母大寫是公開變量,首字母小寫是內部變量,分別至關於類成員變量的 Public 和 Private 類別。內部變量只有屬於同一個 package(簡單理解就是同一個目錄)的代碼才能直接訪問。函數
建立一個結構體變量有多種形式,咱們先看結構體變量最多見的建立形式性能
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle = Circle {
x: 100,
y: 100,
Radius: 50, // 注意這裏的逗號不能少
}
fmt.Printf("%+v\n", c)
}
----------
{x:100 y:100 Radius:50}
複製代碼
經過顯示指定結構體內部字段的名稱和初始值來初始化結構體,能夠只指定部分字段的初值,甚至能夠一個字段都不指定,那些沒有指定初值的字段會自動初始化爲相應類型的「零值」。這種形式咱們稱之爲 「KV 形式」。ui
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
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}
複製代碼
結構體的第二種建立形式是不指定字段名稱來順序字段初始化,須要顯示提供全部字段的初值,一個都不能少。這種形式稱之爲「順序形式」。this
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle = Circle {100, 100, 50}
fmt.Printf("%+v\n", c)
}
-------
{x:100 y:100 Radius:50}
複製代碼
結構體變量和普通變量都有指針形式,使用取地址符就能夠獲得結構體的指針類型spa
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c *Circle = &Circle {100, 100, 50}
fmt.Printf("%+v\n", c)
}
-----------
&{x:100 y:100 Radius:50}
複製代碼
注意上面的輸出,指針形式多了一個地址符 &,表示打印的對象是一個指針類型。介紹完告終構體變量的指針形式,下面就能夠引入結構體變量建立的第三種形式,使用全局的 new() 函數來建立一個「零值」結構體,全部的字段都被初始化爲相應類型的零值。指針
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c *Circle = new(Circle)
fmt.Printf("%+v\n", c)
}
----------
&{x:0 y:0 Radius:0}
複製代碼
注意 new() 函數返回的是指針類型。下面再引入結構體變量的第四種建立形式,這種形式也是零值初始化,就數它看起來最不雅觀。code
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle
fmt.Printf("%+v\n", c)
}
複製代碼
最後咱們再將三種零值初始化形式放到一塊兒對比觀察一下
var c1 Circle = Circle{}
var c2 Circle
var c3 *Circle = new(Circle)
複製代碼
nil 結構體是指結構體指針變量沒有指向一個實際存在的內存。這樣的指針變量只會佔用 1 個指針的存儲空間,也就是一個機器字的內存大小。
var c *Circle = nil
複製代碼
而零值結構體是會實實在在佔用內存空間的,只不過每一個字段都是零值。若是結構體裏面字段很是多,那麼這個內存空間佔用確定也會很大。
Go 語言的 unsafe 包提供了獲取結構體內存佔用的函數 Sizeof()
package main
import "fmt"
import "unsafe"
type Circle struct {
x int
y int
Radius int
}
func main() {
var c Circle = Circle {Radius: 50}
fmt.Println(unsafe.Sizeof(c))
}
-------
24
複製代碼
Circle 結構體在個人 64位機器上佔用了 24 個字節,由於每一個 int 類型都是 8 字節。在 32 位機器上,Circle 結構體只會佔用 12 個字節。
結構體之間能夠相互賦值,它在本質上是一次淺拷貝操做,拷貝告終構體內部的全部字段。結構體指針之間也能夠相互賦值,它在本質上也是一次淺拷貝操做,不過它拷貝的僅僅是指針地址值,結構體的內容是共享的。
package main
import "fmt"
type Circle struct {
x int
y int
Radius int
}
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)
c1.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:50}
&{x:0 y:0 Radius:50}
複製代碼
試試解釋一下上面的輸出結果
經過觀察 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桶地址
...
}
複製代碼
在數組與切片章節,咱們自習分析了數組與切片在內存形式上的區別。數組只有「體」,切片除了「體」以外,還有「頭」部。切片的頭部和內容體是分離的,使用指針關聯起來。請讀者嘗試解釋一下下面代碼的輸出結果
package main
import "fmt"
import "unsafe"
type ArrayStruct struct {
value [10]int
}
type SliceStruct struct {
value []int
}
func main() {
var as = ArrayStruct{[...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
var ss = SliceStruct{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))
}
-------------
80 24
複製代碼
注意代碼中的數組初始化使用了 [...] 語法糖,表示讓編譯器自動推導數組的長度。
函數調用時參數傳遞結構體變量,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}
複製代碼
從上面的輸出中能夠看到經過值傳遞,在函數裏面修改結構體的狀態不會影響到原有結構體的狀態,函數內部的邏輯並無產生任何效果。經過指針傳遞就不同。
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++ 語言裏,值訪問使用句點 . 操做符,而指針訪問須要使用箭頭 -> 操做符。
若是使用上面的方法形式給 Circle 增長一個擴大半徑的方法,你會發現半徑擴大不了。
func (c Circle) expand() {
c.Radius *= 2
}
複製代碼
這是由於上面的方法和前面的 expandByValue 函數是等價的,只不過是把函數的第一個參數挪了位置而已,參數傳遞時會複製了一份結構體內容,起不到擴大半徑的效果。這時候就必需要使用結構體的指針方法
func (c *Circle) expand() {
c.Radius *= 2
}
複製代碼
結構體指針方法和值方法在調用時形式上是沒有區別的,只不過一個能夠改變結構體內部狀態,而另外一個不會。指針方法使用結構體值變量能夠調用,值方法使用結構體指針變量也能夠調用。
經過指針訪問內部的字段須要 2 次內存讀取操做,第一步是取得指針地址,第二部是讀取地址的內容,它比值訪問要慢。可是在方法調用時,指針傳遞能夠避免結構體的拷貝操做,結構體比較大時,這種性能的差距就會比較明顯。
還有一些特殊的結構體它不容許被複制,好比結構體內部包含有鎖時,這時就必須使用它的指針形式來定義方法,不然會發生一些莫名其妙的問題。
結構體做爲一種變量它能夠放進另一個結構體做爲一個字段來使用,這種內嵌結構體的形式在 Go 語言裏稱之爲「組合」。下面咱們來看看內嵌結構體的基本使用方法
package main
import "fmt"
type Point struct {
x int
y int
}
func (p Point) show() {
fmt.Println(p.x, p.y)
}
type Circle struct {
loc Point
Radius int
}
func main() {
var c = Circle {
loc: Point {
x: 100,
y: 100,
},
Radius: 50,
}
fmt.Printf("%+v\n", c)
fmt.Printf("%+v\n", c.loc)
fmt.Printf("%d %d\n", c.loc.x, c.loc.y)
c.loc.show()
}
----------------
{loc:{x:100 y:100} Radius:50}
{x:100 y:100}
100 100
100 100
複製代碼
還有一種特殊的內嵌結構體形式,內嵌的結構體不提供名稱。這時外面的結構體將直接繼承內嵌結構體全部的內部字段和方法,就好像把子結構體的一切所有都揉進了父結構體同樣。匿名的結構體字段將會自動得到以結構體類型的名字命名的字段名稱
package main
import "fmt"
type Point struct {
x int
y int
}
func (p Point) show() {
fmt.Println(p.x, p.y)
}
type Circle struct {
Point // 匿名內嵌結構體
Radius int
}
func main() {
var c = Circle {
Point: Point {
x: 100,
y: 100,
},
Radius: 50,
}
fmt.Printf("%+v\n", c)
fmt.Printf("%+v\n", c.Point)
fmt.Printf("%d %d\n", c.x, c.y) // 繼承了字段
fmt.Printf("%d %d\n", c.Point.x, c.Point.y)
c.show() // 繼承了方法
c.Point.show()
}
-------
{Point:{x:100 y:100} Radius:50}
{x:100 y:100}
100 100
100 100
100 100
100 100
複製代碼
這裏的繼承僅僅是形式上的語法糖,c.show() 被轉換成二進制代碼後和 c.Point.show() 是等價的,c.x 和 c.Point.x 也是等價的。
Go 語言不是面嚮對象語言在於它的結構體不支持多態,它不能算是一個嚴格的面嚮對象語言。多態是指父類定義的方法能夠調用子類實現的方法,不一樣的子類有不一樣的實現,從而給父類的方法帶來了多樣的不一樣行爲。下面的例子呈現了 Java 類的多態性。
class Fruit {
public void eat() {
System.out.println("eat fruit");
}
public void enjoy() {
System.out.println("smell first");
eat();
System.out.println("clean finally");
}
}
class Apple extends Fruit {
public void eat() {
System.out.println("eat apple");
}
}
class Banana extends Fruit {
public void eat() {
System.out.println("eat banana");
}
}
public class Main {
public static void main(String[] args) {
Apple apple = new Apple();
Banana banana = new Banana();
apple.enjoy();
banana.enjoy();
}
}
----------------
smell first
eat apple
clean finally
smell first
eat banana
clean finally
複製代碼
父類 Fruit 定義的 enjoy 方法調用了子類實現的 eat 方法,子類的方法能夠對父類定義的方法進行覆蓋,父類的 eat 方法被隱藏起來了。
Go 語言的結構體明確不支持這種形式的多態,外結構體的方法不能覆蓋內部結構體的方法。好比咱們用 Go 語言來改寫上面的水果例子觀察一下輸出結果。
package main
import "fmt"
type Fruit struct {}
func (f Fruit) eat() {
fmt.Println("eat fruit")
}
func (f Fruit) enjoy() {
fmt.Println("smell first")
f.eat()
fmt.Println("clean finally")
}
type Apple struct {
Fruit
}
func (a Apple) eat() {
fmt.Println("eat apple")
}
type Banana struct {
Fruit
}
func (b Banana) eat() {
fmt.Println("eat banana")
}
func main() {
var apple = Apple {}
var banana = Banana {}
apple.enjoy()
banana.enjoy()
}
----------
smell first
eat fruit
clean finally
smell first
eat fruit
clean finally
複製代碼
enjoy 方法調用的 eat 方法仍是 Fruit 本身的 eat 方法,它沒能被外面的結構體方法覆蓋掉。這意味着面向對象的代碼習慣不能直接用到 Go 語言裏了,咱們須要轉變思惟。
面向對象的多態性須要經過 Go 語言的接口特性來模擬,這就是下一節咱們要講的主題。
關注公衆號「碼洞」,閱讀《快學 Go 語言》更多章節