­

Go 結構體

類型別名&定製

類型別名

   類型別名是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 北京市海淀區}}
}

Tags: