Golang學習-CH6 Go語言結構體
- 6.1 Go語言結構體定義
- 6.2 Go語言例項化結構體
- 6.3 初始化結構體的成員變數
- 6.4 Go語言建構函式
- 6.5 Go語言方法和接收器
- 6.6 Go語言為任意型別新增方法
- 6.7 Go語言使用事件系統實現事件的響應和處理
- 6.8 Go語言型別內嵌和結構體內嵌
- 6.9 Go語言結構體內嵌模擬類的繼承
- 6.10 Go語言初始化內嵌結構體
- 6.11 Go語言垃圾回收和SetFinalizer
- 6.12 Go語言結構體部分相關應用示例*
Go 語言通過用自定義的方式形成新的型別,結構體是型別中帶有成員的複合型別。
Go 語言中的型別可以被例項化,使用new
或&
構造的型別例項的型別是型別的指標。
Go語言中結構體與其他語言中類的概念對比:
- Go 語言中沒有“類”的概念,也不支援“類”的繼承等面向物件的概念。
- Go 語言的結構體與“類”都是複合結構體,但 Go 語言中結構體的內嵌配合介面比面向物件具有更高的擴充套件性和靈活性。
- Go 語言不僅認為結構體能擁有方法,且每種自定義型別也可以擁有自己的方法。這個點同C++
6.1 Go語言結構體定義
結構體成員是由一系列的成員變數構成,這些成員變數也被稱為“欄位”。欄位有以下特性:
- 欄位擁有自己的型別和值。
- 欄位名必須唯一。
- 欄位的型別也可以是結構體,甚至是欄位所在結構體的型別。
使用關鍵字 type 可以將各種基本型別定義為自定義型別。結構體是一種複合的基本型別,通過 type 定義為自定義型別後,使結構體更便於使用。
type Point struct {
X int
Y int
}
//同類型變數寫在同一行
type Color struct {
R, G, B byte
}
結構體的定義只是一種記憶體佈局的描述,只有當結構體例項化時,才會真正地分配記憶體。
6.2 Go語言例項化結構體
結構體的定義只是一種記憶體佈局的描述,只有當結構體例項化時,才會真正地分配記憶體,因此必須在定義結構體並例項化後才能使用結構體的欄位。
基本的例項化形式
結構體本身是一種型別,以 var 的方式宣告結構體即可完成例項化。
type Point struct {
X int
Y int
}
var p Point
p.X = 10
p.Y = 20
- 使用
.
來訪問結構體的成員變數,如p.X
和p.Y
等,結構體成員變數的賦值方法與普通變數一致。
new建立指標型別的結構體
Go語言中,還可以使用 new 關鍵字對型別(包括結構體、整型、浮點數、字串等)進行例項化,結構體在例項化後會形成指標型別的結構體。
type Player struct{
Name string
HealthPoint int
MagicPoint int
}
tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300
- 實際上tank為指標型別,但是在Go語言中仍然可以使用
.
來直接訪問結構體指標的成員 - 因為Go語言為了方便開發者訪問結構體指標的成員變數,使用了語法糖(Syntactic sugar)技術,將
ins.Name
形式轉換為(*ins).Name
。
取結構體的地址例項化
在Go語言中,對結構體進行&
取地址操作時,視為對該型別進行一次 new 的例項化操作,取地址格式如下:
ins := &T{} //ins 為結構體的例項,型別為 *T,是指標型別。
type Command struct {
Name string // 指令名稱
Var *int // 指令繫結的變數
Comment string // 指令的註釋
}
var version int = 1
cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"
取地址例項化是最廣泛的一種結構體例項化方式,可以使用函式封裝上面的初始化過程:
func newCommand(name string, varref *int, comment string) *Command {
return &Command{
Name: name,
Var: varref,
Comment: comment,
}
}
6.3 初始化結構體的成員變數
結構體在例項化時可以直接對成員變數進行初始化,初始化有兩種形式分別是以欄位“鍵值對”形式和多個值的列表形式。
鍵值對形式的初始化適合選擇性填充欄位較多的結構體,多個值的列表形式適合填充欄位較少的結構體。
使用“鍵值對”初始化結構體
鍵值對的填充是可選的,不需要初始化的欄位可以不填入初始化列表中。欄位的預設值是欄位型別的預設值。
格式如下:
ins := 結構體型別名{
欄位1: 欄位1的值,
欄位2: 欄位2的值,
…
}
示例如下:
type People struct {
name string
child *People
}
relation := &People{
name: "爺爺",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
結構體成員中只能包含自身結構體的指標型別,包含非指標型別會引起編譯錯誤。
類似C++的情況,不知道記憶體分佈時無法宣告實體。
使用多個值的列表初始化結構體
Go語言可以在“鍵值對”初始化的基礎上忽略“鍵”,但是:
- 必須初始化結構體的所有欄位。
- 每一個初始值的填充順序必須與欄位在結構體中的宣告順序一致。
- 鍵值對與值列表的初始化形式不能混用。
格式如下:
ins := 結構體型別名{
欄位1的值,
欄位2的值,
…
}
示例如下:
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{
"四川",
"成都",
610000,
"0",
}
fmt.Println(addr)
初始化匿名結構體
匿名結構體沒有型別名稱,無須通過 type 關鍵字定義就可以直接使用。
// 列印訊息型別, 傳入匿名結構體
func printMsgType(msg *struct {
id int
data string
}) {
// 使用動詞%T列印msg的型別
fmt.Printf("%T\n", msg)
}
func main() {
// 例項化一個匿名結構體
msg := &struct { // 定義部分
id int
data string
}{ // 值初始化部分
1024,
"hello",
}
printMsgType(msg)
}
匿名結構體的型別名是結構體包含欄位成員的詳細描述,匿名結構體在使用時需要重新定義,造成大量重複的程式碼,因此開發中較少使用。
6.4 Go語言建構函式
Go語言的型別或結構體沒有建構函式的功能,但是我們可以使用結構體初始化的過程來模擬實現建構函式。
其他程式語言建構函式的一些常見功能及特性如下:
- 每個類可以新增建構函式,多個建構函式使用函式過載實現。Go語言沒有過載
- 建構函式一般與類名同名,且沒有返回值。
- 建構函式有一個靜態建構函式,一般用這個特性來呼叫父類的建構函式。
- 對於 C++ 來說,還有預設建構函式、拷貝建構函式等。
模擬建構函式過載
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{
Name: name,
}
}
func NewCatByColor(color string) *Cat {
return &Cat{
Color: color,
}
}
模擬父子建構函式呼叫
type Cat struct {
Color string
Name string
}
type BlackCat struct {
Cat // 嵌入Cat, 類似於派生
}
// “構造基類”
func NewCat(name string) *Cat {
return &Cat{
Name: name,
}
}
// “構造子類”
func NewBlackCat(color string) *BlackCat {
cat := &BlackCat{}
cat.Color = color
return cat
}
總之,Go語言中沒有提供建構函式相關的特殊機制,使用者根據自己的需求,將引數使用函式傳遞到結構體構造引數中即可完成建構函式的任務。
6.5 Go語言方法和接收器
- 在Go語言中有一個概念,它和方法有著同樣的名字,並且大體上意思相同,Go 方法是作用在接收器(receiver)上的一個函式,接收器是某種型別的變數,因此方法是一種特殊型別的函式。
- 接收器型別可以是(幾乎)任何型別,不僅僅是結構體型別,任何型別都可以有方法,甚至可以是函式型別,可以是 int、bool、string 或陣列的別名型別,但是接收器不能是一個介面型別,因為介面是一個抽象定義,而方法卻是具體實現,如果這樣做了就會引發編譯錯誤。
- 一個型別加上它的方法等價於面向物件中的一個類,一個重要的區別是,在Go語言中,型別的程式碼和繫結在它上面的方法的程式碼可以不放置在一起,它們可以存在不同的原始檔中,唯一的要求是它們必須是同一個包的。
- 型別 T(或 T)上的所有方法的集合叫做型別 T(或 T)的方法集。
- 因為方法是函式,所以同樣的,不允許方法過載,即對於一個型別只能有一個給定名稱的方法,但是如果基於接收器型別,是有過載的:具有同樣名字的方法可以在 2 個或多個不同的接收器型別上存在,比如在同一個包裡這麼做是允許的。
為結構體新增方法
type Bag struct {
items []int
}
func (b *Bag) Insert(itemid int) {
b.items = append(b.items, itemid)
}
func main() {
b := new(Bag)
b.Insert(1001)
}
接收器說明
- 接收器變數:接收器中的引數變數名在命名時,官方建議使用接收器型別名的第一個小寫字母,而不是 self、this 之類的命名。例如,Socket 型別的接收器變數應該命名為 s,Connector 型別的接收器變數應該命名為 c 等。
- 接收器型別:接收器型別和引數類似,可以是指標型別和非指標型別。兩種接收器在使用時會產生不同的效果,根據效果的不同,兩種接收器會被用於不同效能和功能要求的程式碼中。
指標型別的接收器:
- 指標型別的接收器由一個結構體的指標組成,更接近於面向物件中的 this 或者 self。由於指標的特性,呼叫方法時,修改接收器指標的任意成員變數,在方法結束後,修改都是有效的。
非指標型別的接收器:
當方法作用於非指標接收器時,Go語言會在程式碼執行時將接收器的值複製一份,在非指標接收器的方法中可以獲取接收器的成員值,但修改後無效。
// 定義點結構
type Point struct {
X int
Y int
}
// 非指標接收器的加方法
func (p Point) Add(other Point) Point {
// 成員值與引數相加後返回新的結構
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
// 初始化點
p1 := Point{1, 1}
p2 := Point{2, 2}
// 與另外一個點相加
result := p1.Add(p2)
// 輸出結果
fmt.Println(result)
}
在計算機中,小物件由於值複製時的速度較快,所以適合使用非指標接收器,大物件因為複製效能較低,適合使用指標接收器,在接收器和引數間傳遞時不進行復制,只是傳遞指標。
6.6 Go語言為任意型別新增方法
Go語言可以對任何型別新增方法,結構體也是一種型別。
- 在Go語言中,使用 type 關鍵字可以定義出新的自定義型別,之後就可以為自定義型別新增各種方法了。
// 將int定義為MyInt型別
type MyInt int
// 為MyInt新增IsZero()方法
func (m MyInt) IsZero() bool {
return m == 0
}
// 為MyInt新增Add()方法
func (m MyInt) Add(other int) int {
return other + int(m)
}
func main() {
var b MyInt
fmt.Println(b.IsZero())
b = 1
fmt.Println(b.Add(2))
}
- 但是不能直接基於本地型別定義方法,編譯錯誤
6.7 Go語言使用事件系統實現事件的響應和處理
Go語言可以將型別的方法與普通函式視為一個概念,從而簡化方法和函式混合作為回撥型別時的複雜性。這個特性和 C# 中的代理(delegate)類似呼叫者無須關心誰來支援呼叫,系統會自動處理是否呼叫普通函式或型別的方法。
方法和函式的統一呼叫
// 宣告一個結構體
type class struct {
}
// 給結構體新增Do方法
func (c *class) Do(v int) {
fmt.Println("1 call method do:", v)
}
// 普通函式的Do
func funcDo(v int) {
fmt.Println("2 call function do:", v)
}
func main() {
// 宣告一個函式回撥
var delegate func(int)
// 建立結構體例項
c := new(class)
// 將回調設為c的Do方法
delegate = c.Do
// 呼叫
delegate(100)
// 將回調設為普通函式
delegate = funcDo
// 呼叫
delegate(100)
}
無論是普通函式還是結構體的方法,只要它們的簽名一致,與它們簽名一致的函式變數就可以儲存普通函式或是結構體方法。實現了介面統一。
簽名一致:函式型別一致
事件系統基本原理*
事件系統可以將事件派發者與事件處理者解耦。
例如,網路底層可以生成各種事件,在網路連線上後,網路底層只需將事件派發出去,而不需要關心到底哪些程式碼來響應連線上的邏輯。或者再比如,你註冊、關注或者訂閱某“大V”的社交訊息後,“大V”發生的任何事件都會通知你,但他並不用瞭解粉絲們是如何為她喝彩或者瘋狂的。
一個事件系統擁有如下特性:
- 能夠實現事件的一方,可以根據事件 ID 或名字註冊對應的事件。
- 事件發起者,會根據註冊資訊通知這些註冊者。
- 一個事件可以有多個實現方響應。
6.8 Go語言型別內嵌和結構體內嵌
結構體可以包含一個或多個匿名(或內嵌)欄位,即這些欄位沒有顯式的名字,只有欄位的型別是必須的,此時型別也就是欄位的名字。
匿名欄位本身可以是一個結構體型別,即結構體可以包含內嵌結構體。
Go語言中的繼承是通過內嵌或組合來實現的,所以可以說,在Go語言中,相比較於繼承,組合更受青睞。
type innerS struct {
in1 int
in2 int
}
type outerS struct {
b int
c float32
int // anonymous field
innerS //anonymous field
}
func main() {
outer := new(outerS)
outer.b = 6
outer.c = 7.5
outer.int = 60
outer.in1 = 5
outer.in2 = 10
fmt.Printf("outer.b is: %d\n", outer.b)
fmt.Printf("outer.c is: %f\n", outer.c)
fmt.Printf("outer.int is: %d\n", outer.int)
fmt.Printf("outer.in1 is: %d\n", outer.in1)
fmt.Printf("outer.in2 is: %d\n", outer.in2)
// 使用結構體字面量
outer2 := outerS{6, 7.5, 60, innerS{5, 10}}
fmt.Printf("outer2 is:", outer2)
}
在一個結構體中對於每一種資料型別只能有一個匿名欄位。
內嵌結構體
Go語言的結構體內嵌有如下特性:
- 內嵌的結構體可以直接訪問其成員變數
- 編譯器在發現可能的賦值歧義時會報錯。
6.9 Go語言結構體內嵌模擬類的繼承
Go語言的結構體內嵌特性就是一種組合特性,使用組合特性可以快速構建物件的不同特性。
package main
import "fmt"
// 可飛行的
type Flying struct{}
func (f *Flying) Fly() {
fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
fmt.Println("can calk")
}
// 人類
type Human struct {
Walkable // 人類能行走
}
// 鳥類
type Bird struct {
Walkable // 鳥類能行走
Flying // 鳥類能飛行
}
func main() {
// 例項化鳥類
b := new(Bird)
fmt.Println("Bird: ")
b.Fly()
b.Walk()
// 例項化人類
h := new(Human)
fmt.Println("Human: ")
h.Walk()
}
使用Go語言的內嵌結構體實現物件特性,可以自由地在物件中增、刪、改各種特性。Go語言會在編譯時檢查能否使用這些特性。
6.10 Go語言初始化內嵌結構體
// 車輪
type Wheel struct {
Size int
}
// 車
type Car struct {
Wheel
// 引擎
Engine struct {
Power int // 功率
Type string // 型別
}
}
func main() {
c := Car{
// 初始化輪子-型別名做欄位名
Wheel: Wheel{
Size: 18,
},
// 初始化引擎-匿名型別
Engine: struct {
Power int
Type string
}{
Type: "1.4T",
Power: 143,
},
}
fmt.Printf("%+v\n", c)
}
6.11 Go語言垃圾回收和SetFinalizer
Go語言自帶垃圾回收機制(GC)。GC 通過獨立的程序執行,它會搜尋不再使用的變數,並將其釋放。需要注意的是,GC 在執行時會佔用機器資源。
GC 是自動進行的,如果要手動進行 GC,可以使用 runtime.GC()
函式,顯式的執行 GC。顯式的進行 GC 只在某些特殊的情況下才有用,比如當記憶體資源不足時呼叫 runtime.GC() ,這樣會立即釋放一大片記憶體,但是會造成程式短時間的效能下降。
finalizer(終止器)是與物件關聯的一個函式,通過 runtime.SetFinalizer 來設定,如果某個物件定義了 finalizer,當它被 GC 時候,這個 finalizer 就會被呼叫,以完成一些特定的任務,例如發訊號或者寫日誌等。
在Go語言中 SetFinalizer 函式是這樣定義的:
func SetFinalizer(x, f interface{})
引數說明如下:
- 引數 x 必須是一個指向通過 new 申請的物件的指標,或者通過對複合字面值取址得到的指標。
- 引數 f 必須是一個函式,它接受單個可以直接用 x 型別值賦值的引數,也可以有任意個被忽略的返回值。
SetFinalizer 函式可以將 x 的終止器設定為 f,當垃圾收集器發現 x 不能再直接或間接訪問時,它會清理 x 並呼叫 f(x)。
另外,x 的終止器會在 x 不能直接或間接訪問後的任意時間被呼叫執行,不保證終止器會在程式退出前執行,因此一般終止器只用於在長期執行的程式中釋放關聯到某物件的非記憶體資源。
終止器會按依賴順序執行:如果 A 指向 B,兩者都有終止器,且 A 和 B 沒有其它關聯,那麼只有 A 的終止器執行完成,並且 A 被釋放後,B 的終止器才可以執行。
如果 *x 的大小為 0 位元組,也不保證終止器會執行。
此外,我們也可以使用SetFinalizer(x, nil)
來清理繫結到 x 上的終止器。
提示:終止器只有在物件被 GC 時,才會被執行。其他情況下,都不會被執行,即使程式正常結束或者發生錯誤。
package main
import (
"log"
"runtime"
"time"
)
type Road int
func findRoad(r *Road) {
log.Println("road:", *r)
}
func entry() {
var rd Road = Road(999)
r := &rd
runtime.SetFinalizer(r, findRoad)
}
func main() {
entry()
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
runtime.GC()
}
}