Go 結構體

雲崖先生發表於2020-10-07

型別別名&定製

型別別名

   型別別名是Go的1.9版本中新新增的功能。

   大概意思就是給一個型別取一個別名,小名等,但是這個別名還是指向的相同型別。

   如uint32的別名rune,其底層還是uint32

   如uint8的別名byte,使用byte實際上還是uint8

   別名的作用在於在程式設計中更方便的進行使用型別,如下示例,我們為int64取一個別名long

package main

import (
	"fmt"
)

func main() {
	type long = int64
	var num long
	num = 100000000
	fmt.Printf("%T %v", num, num)
	// int64 100000000
}

自定型別

   自定型別類似於繼承某個內建的型別,它會以一種全新的型別出現,並且我們可以為該自定型別做一些定製方法。

   如下定義的small型別,是基於uint8的一個型別。它是一種全新的型別,但是具有uint8的特性。

package main

import (
	"fmt"
)

func main() {
	type small uint8
	var num small = 32
	fmt.Printf("%T %v", num, num)
	// main.small 32
}

區別差異

   可以看到上面示例中的列印結果

// int64 100000000
// main.small 32

   結果顯示自定義型別是main.small,其實自定義型別只在程式碼中存在,編譯時會將其轉換為uint8

結構體

   結構體類似於其他語言中的物件導向,值得一提的是Go語言是一種面向介面的語言,所以弱化了對物件導向方面的處理。

   在結構體中,我們可以清晰的表示一個現實中的事物,注意:結構體其實就是一種自定義的型別。

   在Go中使用struct來定義結構體。

   以下是語法介紹,typestruct這兩個關鍵字用於定義結構體。

type 型別名 struct {
    欄位名 欄位型別
    欄位名 欄位型別
    …
}

   型別名:標識自定義結構體的名稱,在同一個包內不能重複。如果不是對外開放的介面,則首字母小寫,否則大寫。

   欄位名:表示結構體欄位名。結構體中的欄位名必須唯一。

   欄位型別:表示結構體欄位的具體型別。

   下面我們來定義一個dog的結構體。

// 命名小寫,表示dog結構體不對外開放
type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

.例項化

   當結構體定義完成後,必須對其進行例項化後才可使用。

   單純的定義結構體是不會分配記憶體的。

   以下將介紹通過.進行例項化。

基本例項化

   下面是基本例項化的示例。

   首先定義一個變數,宣告它是dog型別,再通過.對其中的欄位進行賦值。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	var d1 dog
	d1.dogName = "大黃"
	d1.dogAge = 12
	d1.dogGender = true
	fmt.Println(d1)
	// {大黃 12 true}
}

匿名結構體

   有的結構體只使用一次,那麼就可以使用匿名結構體在定義之初對其進行例項化。

   這個時候只使用stuct即可,不必使用type進行型別的自定義。

package main

import (
	"fmt"
)


func main() {
	var dog struct{
		dogName string 
		dogAge int8
		dogGender bool
	}
	dog.dogName = "大黃"
	dog.dogAge = 12
	dog.dogGender = true
	fmt.Println(dog)
	// {大黃 12 true}
}

指標例項化

   通過new可以對結構體進行例項化,具體步驟是拿到其結構體指標後通過.對其欄位填充,進而達到例項化的目的。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	var d1 = new(dog) // 拿到結構體指標
	d1.dogName = "大黃"
	d1.dogAge = 12
	d1.dogGender = true
	fmt.Println(d1)
	// &{大黃 12 true}
}

地址例項化

   使用&對結構體進行取地址操作相當於對該結構體型別進行了一次new例項化操作。

   與上面的方式本質都是相同的。

   d1.dogName= "大黃"其實在底層是(*d1).dogName= "大黃",這是Go語言幫我們實現的語法糖。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := &dog{}
	d1.dogName = "大黃"
	d1.dogAge = 12
	d1.dogGender = true
	fmt.Println(d1)
	// &{大黃 12 true}
}

{}例項化

   例項化時,除開可以使用.也可以使用{}

   在實際開發中使用{}例項化的普遍更多。

基本例項化

   以下是使用{}進行基本例項化的示例。

   key對應欄位名,value對應例項化的填充值。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := dog{
		dogName:"大黃",
		dogAge:12,
		dogGender:true,
	}
	fmt.Print(d1)
}

順序例項化

   可以不填入key對其進行例項化,但是要與定義結構體時的欄位位置一一對應。

  1. 必須初始化結構體的所有欄位。
  2. 初始值的填充順序必須與欄位在結構體中的宣告順序一致。
  3. 該方式不能和鍵值初始化方式混用。
package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := dog{
		"大黃",
		12,
		true,
	}
	fmt.Print(d1)
}

地址例項化

   下面是使用{}進行地址例項化。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := &dog{
		"大黃",
		12,
		true,
	}
	fmt.Print(d1)
}

記憶體佈局

連續記憶體

   一個結構體中的欄位,都是佔據一整塊連續記憶體。

   但有的欄位可能看起來不會與前一個欄位進行相鄰,這是受到型別的影響,具體可檢視:在 Go 中恰到好處的記憶體對齊

package main

import (
	"fmt"
)

type dog struct {
	dogName   string
	dogAge    int8
	dogGender bool
}

func main() {
	d1 := &dog{
		"大黃",
		12,
		true,
	}
	fmt.Printf("%p \n", &d1.dogName)
	fmt.Printf("%p \n", &d1.dogAge)
	fmt.Printf("%p \n", &d1.dogGender)

	// 0xc0000044a0
	// 0xc0000044b0
	// 0xc0000044b1

}

空結構體

   一個空的結構體是不佔據任何記憶體的。

package main

import (
	"fmt"
	"unsafe"
)


func main() {
	var dog struct{}
	fmt.Print(unsafe.Sizeof(dog)) // 0 檢視佔據的記憶體
}

建構函式

   當一個函式返回一個結構體例項時,該函式將被稱為建構函式。

   Go語言中沒有建構函式,但是我們可以自己進行定義。

   注意建構函式的命名方式要用new進行開頭。

普通構造

   由於函式的引數傳遞都是值傳遞,所以每次需要將結構體拷貝到建構函式中,這是非常消耗記憶體的。

   所以在真實開發中不應該使用這種方式

package main

import (
	"fmt"
)

type dog struct {
	dogName string
	dogAge int8
	dogGender bool
}

// dog每次都會進行拷貝,消耗記憶體
func newDog(dogName string, dogAge int8, dogGender bool) dog {
	return dog{
		dogName: dogName,
		dogAge: dogAge,
		dogGender: dogGender,
	}
}

func main(){
	d1 := newDog("大黃",12,true)
	fmt.Printf("%p \n",&d1)
}

指標構造

   如果讓其使用指標構造,就不用每次都會進行拷貝了。推薦使用該方式。

   只需要加上*&即可。

package main

import (
	"fmt"
)

type dog struct {
	dogName string
	dogAge int8
	dogGender bool
}

// 每次的傳遞都是dog的指標,所以不會進行值拷貝
func newDog(dogName string, dogAge int8, dogGender bool) *dog {
	return &dog{
		dogName: dogName,
		dogAge: dogAge,
		dogGender: dogGender,
	}
}

func main(){
	d1 := newDog("大黃",12,true)
	fmt.Printf("%p \n",&d1)
}

方法&接收者

   為任意型別定製一個自定義方法,必須要為該型別進行接收者限制。

   接收者類似於其他語言中的selfthis

   定義方法格式如下:

func (接收者變數 接收者型別) 方法名(引數列表) (返回引數) {
    函式體
}

   接收者變數:接收者中的引數變數名在命名時,官方建議使用接收者型別名稱首字母的小寫,而不是selfthis之類的命名。例如,Person型別的接收者變數應該命名為 pConnector型別的接收者變數應該命名為c等。

   接收者型別:接收者型別和引數類似,可以是指標型別和非指標型別。

   方法名、引數列表、返回引數:具體格式與函式定義相同。

普通接收者

   以下示例是定義普通接收者方法。

   注意,這是值拷貝,意味著你的d會拷貝d1的資料。

package main

import (
	"fmt"
)

type dog struct {
	dogName string
	dogAge int8
	dogGender bool
}

func newDog(dogName string, dogAge int8, dogGender bool) *dog {
	return &dog{
		dogName: dogName,
		dogAge: dogAge,
		dogGender: dogGender,
	}
}

func (d dog)getAge() int8 {
	return d.dogAge  // 返回狗狗的年齡
}

func main(){
	d1 := newDog("大黃",12,true)
	age := d1.getAge()
	fmt.Print(age) // 12
}

指標接收者

   由於普通接收者方法無法做到修改原本例項化物件資料的需求,所以我們可以定義指標接收者方法進行引用傳遞。

   如下,呼叫addAge()方法會將原本的年齡加上十歲。

package main

import (
	"fmt"
)

type dog struct {
	dogName   string
	dogAge    int8
	dogGender bool
}

func newDog(dogName string, dogAge int8, dogGender bool) *dog {
	return &dog{
		dogName:   dogName,
		dogAge:    dogAge,
		dogGender: dogGender,
	}
}

func (d *dog) addAge() {
	d.dogAge += 10
}

func main() {
	d1 := newDog("大黃", 12, true)
	fmt.Printf("舊年齡:%v", d1.dogAge) // 12
	d1.addAge()
	fmt.Printf("新年齡:%v", d1.dogAge) // 22
}

   關於使用d1直接呼叫,這是一種語法糖形式。完整的形式應該是使用&取到地址後再進行傳遞,但是這樣會出現一些問題。

   所以直接使用例項化物件呼叫即可。

   下面是關於指標方法的一些使用注意事項:

  1. 修改原本例項化物件中的值時,應該使用指標接收者方法
  2. 例項化物件的內容較多,拷貝代價較大時,應該使用指標接收者方法
  3. 如果該物件下某個方法是指標接收者方法,那麼為了保持一致性,其他方法也應該使用指標方法。

自定型別方法

   下面將對自定義型別small做一個方法,getBool獲取其布林值。

package main

import (
	"fmt"
)

type small uint8


func (s small) getBool() bool {
	if s != 0 {
		return true
	}
	return false
}

func main() {
	var num small
	num = 18
	result := num.getBool()
	fmt.Print(result) // true
}

  

匿名欄位

基本使用

   匿名欄位即只使用欄位型別,不使用欄位名。

   使用較少

   注意:這裡匿名欄位的說法並不代表沒有欄位名,而是預設會採用型別名作為欄位名,結構體要求欄位名稱必須唯一,因此一個結構體中同種型別的匿名欄位只能有一個。

package main

import (
	"fmt"
)

type dog struct {
	string // 只能出現一次同型別的欄位。
	int8
	bool
}

func main() {
	d1 := dog{
		string: "大黃",
		int8:   12,
		bool:   true,
	}
	fmt.Print(d1)
}

結構體巢狀

基本使用

   一個結構體中可以巢狀另一個結構體。

   通過這種方式,可以達到繼承的效果。

package main

import (
	"fmt"
)

type details struct {
	phone string // 電話
	addr  string // 地址
}

type person struct {
	name    string
	gender  bool
	age     int8
	details // 匿名欄位,詳細資訊
}

func main() {
	p1 := person{
		name:   "雲崖",
		gender: true,
		age:    18,
		details: details{  // 對匿名欄位的巢狀結構體進行例項化
			phone: "1008611",
			addr:  "北京市海淀區",
		},
	}
	fmt.Print(p1)
	// {雲崖 true 18 {1008611 北京市海淀區}}
}

匿名簡寫

   如果要訪問上例中的電話,可以使用簡寫形式。也可以使用全寫形式。

   查詢順序是先查詢具名欄位,再查詢匿名欄位。

   要注意多個結構體巢狀產生的欄位名衝突問題。

package main

import (
	"fmt"
)

type details struct {
	phone string // 電話
	addr  string // 地址
}

type person struct {
	name    string
	gender  bool
	age     int8
	details // 匿名欄位,詳細資訊
}

func main() {
	p1 := person{
		name:   "雲崖",
		gender: true,
		age:    18,
		details: details{  // 對匿名欄位的巢狀結構體進行例項化
			phone: "1008611",
			addr:  "北京市海淀區",
		},
	}
	fmt.Println(p1.phone)  // 簡寫
	fmt.Println(p1.details.phone) // 全寫

}

JSON

   使用JSON包可對結構體進行序列化操作。

   常用於前後端資料互動。

欄位可見性

   由於json包是再encoding/json中,所以我們要想讓main包的結構體能被json包訪問,需要將結構體名字,欄位名字等進行首字母大寫。

   結構體中欄位大寫開頭表示可公開訪問,小寫表示私有(僅在定義當前結構體的包中可訪問)。

基本使用

   以下是關於JSON序列化與反序列化的基本使用。

package main

import (
	"encoding/json" // 導包
	"fmt"
)

// Details 詳情  對於大寫的結構體,應該具有註釋。注意空格
type Details struct {
	Phone string // 電話
	Addr  string // 地址
}

// Person 人
type Person struct {
	Name    string
	Gender  bool
	Age     int8
	Details // 匿名欄位,詳細資訊
}

func main() {
	p1 := Person{
		Name:   "雲崖",
		Gender: true,
		Age:    18,
		Details: Details{ // 對匿名欄位的巢狀結構體進行例項化
			Phone: "1008611",
			Addr:  "北京市海淀區",
		},
	}
	// 序列化 得到一個[]bytes型別
	data, err := json.Marshal(p1)
	if err != nil {
		fmt.Println("json error")
		return
	}
	fmt.Println(string(data)) // 檢視結果
	// {"Name":"雲崖","Gender":true,"Age":18,"Phone":"1008611","Addr":"北京市海淀區"}

	// 反序列化
	p2 := Person{}
	json.Unmarshal(data, &p2) // 反序列化時需要例項化出該結構體。通過地址對其進行賦值
	fmt.Println(p2)           // {雲崖 true 18 {1008611 北京市海淀區}}
}

標籤使用

   我們可以看到上面的序列化後的結果欄位名都是大寫名字開頭的。

{"Name":"雲崖","Gender":true,"Age":18,"Phone":"1008611","Addr":"北京市海淀區"}

   怎麼樣把它轉換為小寫?這個需要使用到結構體標籤。

   示例如下:

package main

import (
	"encoding/json" // 導包
	"fmt"
)

// Details 詳情  對於大寫的結構體,應該具有註釋。注意空格
type Details struct {
	// 代表在json轉換中,Phone更名為phone  orm中更名為phone  配置檔案ini中更名為phone
	Phone string `json:"phone" db:"phone" ini:"phone"`
	Addr  string `json:"addr"`
}

// Person 人
type Person struct {
	Name    string `json:"name"`
	Gender  bool   `json:"gender"`
	Age     int8   `json:"age"`
	Details        // 匿名欄位,詳細資訊
}

func main() {
	p1 := Person{
		Name:   "雲崖",
		Gender: true,
		Age:    18,
		Details: Details{ // 對匿名欄位的巢狀結構體進行例項化
			Phone: "1008611",
			Addr:  "北京市海淀區",
		},
	}
	// 序列化 得到一個[]bytes型別
	data, err := json.Marshal(p1)
	if err != nil {
		fmt.Println("json error")
		return
	}
	fmt.Println(string(data)) // 檢視結果
	// {"name":"雲崖","gender":true,"age":18,"phone":"1008611","addr":"北京市海淀區"}

	// 反序列化
	p2 := Person{}
	json.Unmarshal(data, &p2) // 反序列化時需要例項化出該結構體。通過地址對其進行賦值
	fmt.Println(p2)           // {雲崖 true 18 {1008611 北京市海淀區}}
}

相關文章